From 05032c70736bbe94647f68a522f11eb805d367a0 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 23 Jul 2026 15:22:37 +0200 Subject: [PATCH 01/79] hppocampus --- src/codespy/agents/hippocampus/__init__.py | 28 +++ src/codespy/agents/hippocampus/budget.py | 106 ++++++++++++ src/codespy/agents/hippocampus/context_map.py | 146 ++++++++++++++++ src/codespy/agents/hippocampus/hypocampus.py | 163 ++++++++++++++++++ .../agents/hippocampus/modules/__init__.py | 11 ++ .../hippocampus/modules/cartographer.py | 129 ++++++++++++++ .../agents/hippocampus/modules/distiller.py | 141 +++++++++++++++ 7 files changed, 724 insertions(+) create mode 100644 src/codespy/agents/hippocampus/__init__.py create mode 100644 src/codespy/agents/hippocampus/budget.py create mode 100644 src/codespy/agents/hippocampus/context_map.py create mode 100644 src/codespy/agents/hippocampus/hypocampus.py create mode 100644 src/codespy/agents/hippocampus/modules/__init__.py create mode 100644 src/codespy/agents/hippocampus/modules/cartographer.py create mode 100644 src/codespy/agents/hippocampus/modules/distiller.py diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py new file mode 100644 index 0000000..01a2c31 --- /dev/null +++ b/src/codespy/agents/hippocampus/__init__.py @@ -0,0 +1,28 @@ +from codespy.agents.hippocampus.context_map import ( + CacheCandidate, + ContextMap, + Item, + ItemTag, + Operation, + OpType, + SectionName, +) +from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig +from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig +from codespy.agents.hippocampus.hypocampus import Hypocampus, StepResult + +__all__ = [ + "CacheCandidate", + "Cartographer", + "CartographerSig", + "ContextMap", + "Distiller", + "DistillerSig", + "Item", + "ItemTag", + "Operation", + "OpType", + "Hypocampus", + "SectionName", + "StepResult", +] diff --git a/src/codespy/agents/hippocampus/budget.py b/src/codespy/agents/hippocampus/budget.py new file mode 100644 index 0000000..cca885e --- /dev/null +++ b/src/codespy/agents/hippocampus/budget.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import dspy +import tiktoken + +from codespy.agents.hippocampus.context_map import ContextMap + +_ENCODING = tiktoken.get_encoding("o200k_base") +_MAX_INPUT_FIELD_TOKENS = 256 + +def count_tokens(s: str) -> int: + return len(_ENCODING.encode(s)) + +def format_inputs(kwargs: dict) -> str: + """Serialize call inputs (excluding context_map) for the distiller. + + Each field value is truncated to _MAX_INPUT_FIELD_TOKENS so that large + RLM inputs (documents, file dumps) don't blow up the Distiller context. + """ + parts: list[str] = [] + for k, v in kwargs.items(): + if k == "context_map": + continue + text = str(v) + if count_tokens(text) > _MAX_INPUT_FIELD_TOKENS: + lines, kept, tokens = text.splitlines(keepends=True), [], 0 + for line in lines: + lt = count_tokens(line) + if tokens + lt > _MAX_INPUT_FIELD_TOKENS: + kept.append("... (truncated)") + break + kept.append(line) + tokens += lt + text = "".join(kept) + parts.append(f"{k}: {text}") + return "\n".join(parts) + + +# Eviction priority +_SECTION_EVICT_PRIORITY: dict[str, int] = { + "parsing_schema": 0, # evict first — cheap to rediscover + "reusable_results": 1, # agent-derived; can be recomputed + "domain_constants": 2, # exact values worth protecting + "context_roadmap": 3, # protected — structural index + "context_understanding": 4, # most protected — core orientation +} + + +def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: + if count_tokens(cmap.render()) <= budget: + return cmap + item_section: dict[str, str] = { + it.id: sec for sec in cmap.section_names() for it in cmap.section(sec) + } + flat = cmap.all_items() + order = {it.id: i for i, it in enumerate(flat)} + victims = sorted( + flat, + key=lambda it: ( + _SECTION_EVICT_PRIORITY.get(item_section[it.id], 99), + scores.get(it.id, 0), + order[it.id], + ), + ) + removed: set[str] = set() + for v in victims: + removed.add(v.id) + trial = cmap.without(removed) + if count_tokens(trial.render()) <= budget: + return trial + return cmap.without(removed) + +def format_trajectory(pred: dspy.Prediction) -> str: + traj = getattr(pred, "trajectory", None) + if isinstance(traj, list): + parts = [] + for i, entry in enumerate(traj): + parts.append(f"--- Step {i + 1} ---") + if entry.get("reasoning"): + parts.append(f"Reasoning: {entry['reasoning']}") + parts.append(f"Code:\n{entry['code']}") + parts.append(f"Output:\n{entry['output']}") + return "\n".join(parts) + if isinstance(traj, dict): + return "\n".join(f"{k}: {v}" for k, v in traj.items()) + try: + return "\n".join(f"{k}: {v}" for k, v in pred.toDict().items()) + except Exception: + return str(pred) + + +def truncate_trajectory(text: str, max_tokens: int) -> str: + """Keep as many leading steps as fit within max_tokens.""" + if count_tokens(text) <= max_tokens: + return text + lines = text.splitlines(keepends=True) + kept: list[str] = [] + tokens = 0 + for line in lines: + line_tokens = count_tokens(line) + if tokens + line_tokens > max_tokens: + kept.append("... (truncated)\n") + break + kept.append(line) + tokens += line_tokens + return "".join(kept) diff --git a/src/codespy/agents/hippocampus/context_map.py b/src/codespy/agents/hippocampus/context_map.py new file mode 100644 index 0000000..1b5da1b --- /dev/null +++ b/src/codespy/agents/hippocampus/context_map.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from enum import Enum +from typing import Literal + +from pydantic import BaseModel, Field + + +class ItemTag(str, Enum): + """How a context-map item performed in the trajectory just observed. + + - helpful: directly aided orientation or answering; keep. + - harmful: misled the agent or contradicted observations; remove. + - neutral: present but unused this round; keep with no boost. + - stale: no longer reflects the external context; remove. + """ + + HELPFUL = "helpful" + HARMFUL = "harmful" + NEUTRAL = "neutral" + STALE = "stale" + + +class OpType(str, Enum): + """Cartographer edit operations against the context map.""" + + ADD = "ADD" + DELETE = "DELETE" + REPLACE = "REPLACE" + + +SectionName = Literal[ + "context_roadmap", + "context_understanding", + "domain_constants", + "parsing_schema", + "reusable_results", +] + +# Abbreviated prefixes +_SECTION_PREFIX: dict[str, str] = { + "context_roadmap": "cr", + "context_understanding": "cu", + "domain_constants": "dc", + "parsing_schema": "ps", + "reusable_results": "rr", +} + + +class Item(BaseModel): + id: str + content: str + + +class CacheCandidate(BaseModel): + section: SectionName + value: str = Field(description="Compact candidate cache item (<= ~80 tokens).") + transferability: str = Field(description="Kinds of future questions this would help.") + rationale: str = Field(description="Why this is shared understanding, not a one-off fact.") + + +class Operation(BaseModel): + type: OpType + section: SectionName | None = Field(default=None, description="Required for ADD.") + item_id: str | None = Field(default=None, description="Required for DELETE / REPLACE.") + content: str | None = Field(default=None, description="Required for ADD / REPLACE.") + + +class ContextMap(BaseModel): + context_roadmap: list[Item] = Field( + default_factory=list, + description="Index of what the context contains and where to find it", + ) + context_understanding: list[Item] = Field( + default_factory=list, + description="High-level understanding of the context", + ) + domain_constants: list[Item] = Field( + default_factory=list, + description="Exact parameters, formulas, thresholds, reference values, enum sets, and output field requirements", + ) + parsing_schema: list[Item] = Field( + default_factory=list, + description="How to parse and navigate the context's format: delimiters, boundary patterns, field structure", + ) + reusable_results: list[Item] = Field( + default_factory=list, + description="Agent-derived aggregated outputs (counts, distributions, classifications) that multiple questions would need", + ) + next_id: int = Field(default=1, exclude=True) + + @classmethod + def section_names(cls) -> list[str]: + return [n for n in cls.model_fields if n != "next_id"] + + def section(self, name: str) -> list[Item]: + return getattr(self, name) + + def all_items(self) -> list[Item]: + return [it for s in self.section_names() for it in self.section(s)] + + def ids(self) -> set[str]: + return {it.id for it in self.all_items()} + + def render(self) -> str: + lines: list[str] = [] + for sec in self.section_names(): + info = type(self).model_fields[sec] + items = self.section(sec) + lines.append(f"## {sec.upper().replace('_', ' ')}") + if items: + lines.extend(f"[{it.id}] {it.content}" for it in items) + else: + lines.append(f"({info.description})") + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + def apply(self, ops: list[Operation]) -> tuple[ContextMap, list[str]]: + """Return (new map, ids of newly-added items).""" + cm = self.model_copy(deep=True) + new_ids: list[str] = [] + for op in ops: + if op.type == OpType.DELETE and op.item_id: + for sec in cm.section_names(): + lst = cm.section(sec) + lst[:] = [it for it in lst if it.id != op.item_id] + elif op.type == OpType.REPLACE and op.item_id and op.content: + for sec in cm.section_names(): + lst = cm.section(sec) + for i, it in enumerate(lst): + if it.id == op.item_id: + lst[i] = Item(id=it.id, content=op.content) + elif op.type == OpType.ADD and op.section and op.content: + prefix = _SECTION_PREFIX.get(op.section, op.section[:2]) + new_id = f"{prefix}-{cm.next_id:05d}" + cm.section(op.section).append(Item(id=new_id, content=op.content)) + new_ids.append(new_id) + cm.next_id += 1 + return cm, new_ids + + def without(self, ids: set[str]) -> ContextMap: + cm = self.model_copy(deep=True) + for sec in cm.section_names(): + lst = cm.section(sec) + lst[:] = [it for it in lst if it.id not in ids] + return cm diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py new file mode 100644 index 0000000..0ab66be --- /dev/null +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import copy +from dataclasses import dataclass + +import dspy + +from codespy.agents.hippocampus.budget import ( + count_tokens, + evict, + format_inputs, + format_trajectory, + truncate_trajectory, +) +from codespy.agents.hippocampus.context_map import ContextMap, ItemTag +from codespy.agents.hippocampus.modules.cartographer import Cartographer +from codespy.agents.hippocampus.modules.distiller import Distiller + +def prepend_context_map(sig): + return sig.prepend( + name="context_map", + field=dspy.InputField( + desc="Orientation cache about the external context. Use it before redundant tool calls." + ), + type_=ContextMap, + ) + +@dataclass +class StepResult: + diagnosis: str + reasoning: str + operations_applied: int + map_text: str + + +class Hypocampus(dspy.Module): + """Wraps a dspy.Module with a test-time-evolving context map. + + Prepends a `context_map: ContextMap` input field to every predictor + inside the module, then delegates forward() to it. After each call the + trajectory is distilled and the context map is updated. + """ + + def __init__( + self, + module: dspy.Module, + token_budget: int = 1024, + max_trajectory_tokens: int = 4096, + freeze_after: int | None = None, + question_field: str | None = None, + ): + """ + Args: + module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. + token_budget: Maximum tokens kept in the context map. + max_trajectory_tokens: Maximum tokens from the trajectory fed to the Distiller. + freeze_after: Stop updating the map after this many calls (None = always update). + question_field: Name of the input field that carries the task description. + If set, only that field is passed to the Distiller as the "question". + If None, all input fields are serialized and truncated automatically — + useful for simple signatures but lossy when inputs are large (e.g. RLM + with document inputs). Set this explicitly whenever one field cleanly + captures the user's intent. + """ + super().__init__() + + module = copy.deepcopy(module) + + # Prepend context_map only to predictors that receive the module's own + # input fields. + top_sig = getattr(module, "signature", None) + if top_sig is not None: + module_inputs = set(top_sig.input_fields) + module.signature = prepend_context_map(top_sig) + for _, pred in module.named_predictors(): + if set(pred.signature.input_fields) & module_inputs: + pred.signature = prepend_context_map(pred.signature) + else: + for _, pred in module.named_predictors(): + pred.signature = prepend_context_map(pred.signature) + + self.agent = module + self.distill = Distiller() + self.cartograph = Cartographer() + self.token_budget = token_budget + self.max_trajectory_tokens = max_trajectory_tokens + self.freeze_after = freeze_after + self.question_field = question_field + self.cmap = ContextMap() + self.scores: dict[str, int] = {} + self.last_step: StepResult | None = None + self._calls = 0 + self._frozen = False + + @property + def current_map_text(self) -> str: + return self.cmap.render() + + def freeze(self) -> None: + """Stop evolving the context map on subsequent forward() calls.""" + self._frozen = True + + def unfreeze(self) -> None: + self._frozen = False + + def forward(self, **kwargs) -> dspy.Prediction: + pred = self.agent(context_map=self.cmap, **kwargs) + self._calls += 1 + if not self._frozen and ( + self.freeze_after is None or self._calls <= self.freeze_after + ): + self._update_cache(pred, inputs=kwargs) + return pred + + def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: + if self.question_field is not None: + question = str(inputs.get(self.question_field, "")) + else: + question = format_inputs(inputs) + trajectory = truncate_trajectory(format_trajectory(pred), self.max_trajectory_tokens) + distilled = self.distill( + trajectory=trajectory, + context_map=self.cmap, + question=question, + ) + + known = self.cmap.ids() + tags = {k: v for k, v in (distilled.item_tags or {}).items() if k in known} + for bid, tag in tags.items(): + if tag == ItemTag.HELPFUL: + self.scores[bid] = self.scores.get(bid, 0) + 1 + elif tag in (ItemTag.HARMFUL, ItemTag.STALE): + self.scores[bid] = self.scores.get(bid, 0) - 1 + else: + self.scores.setdefault(bid, 0) + + edits = self.cartograph( + diagnosis=distilled.diagnosis, + item_tags=tags, + cache_candidates=list(distilled.cache_candidates or []), + current_map=self.cmap, + question=question, + token_budget=self.token_budget, + current_tokens=count_tokens(self.cmap.render()), + ) + ops = list(edits.operations or []) + + if ops: + self.cmap, new_ids = self.cmap.apply(ops) + for nid in new_ids: + self.scores[nid] = self.scores.get(nid, 0) + 1 + + self.cmap = evict(self.cmap, self.scores, self.token_budget) + + live = self.cmap.ids() + self.scores = {k: v for k, v in self.scores.items() if k in live} + + self.last_step = StepResult( + diagnosis=distilled.diagnosis, + reasoning=edits.reasoning, + operations_applied=len(ops), + map_text=self.cmap.render(), + ) diff --git a/src/codespy/agents/hippocampus/modules/__init__.py b/src/codespy/agents/hippocampus/modules/__init__.py new file mode 100644 index 0000000..2c325bf --- /dev/null +++ b/src/codespy/agents/hippocampus/modules/__init__.py @@ -0,0 +1,11 @@ +"""DSPy modules for the hippocampus agent.""" + +from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig +from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig + +__all__ = [ + "Cartographer", + "CartographerSig", + "Distiller", + "DistillerSig", +] diff --git a/src/codespy/agents/hippocampus/modules/cartographer.py b/src/codespy/agents/hippocampus/modules/cartographer.py new file mode 100644 index 0000000..d40cb2b --- /dev/null +++ b/src/codespy/agents/hippocampus/modules/cartographer.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import dspy + +from codespy.agents.hippocampus.context_map import ( + CacheCandidate, + ContextMap, + ItemTag, + Operation, +) + + +class CartographerSig(dspy.Signature): + """You are a context map curator. You maintain a concise, high-value + context map prepended to an agent that repeatedly interacts with a long + external context. + + The context map captures the agent's evolving UNDERSTANDING of the + context — NOT answers to specific questions. Think of it as the mental + model a human builds after reading a document: structure, key entities, + relationships, and global summaries that help with ANY question about + the content. + + ## Instructions + + - Review the latest Distiller diagnosis and the current context map. + - Prioritize items representing SHARED UNDERSTANDING — knowledge + useful across many different questions on this context. + - Demote or remove question-specific facts that only help one query. + - Keep items that are structural, relational, or globally informative. + - Remove items that are stale, misleading, redundant, or low-value. + - Rewrite items when a more compact or more useful version exists. + Prefer REPLACE over ADD when possible. + - Add new items only when they represent transferable understanding. + - Each item must be short and budget-efficient — max ~80 tokens per + item. If a candidate exceeds this, rewrite it more compactly or + split it. + - If nothing new is worth keeping, return an empty operations list. + + The litmus test: For each item, ask "Would a future agent asking a + completely DIFFERENT question about this context benefit from knowing + this?" If not, it probably isn't worth the budget. + + ## Value Priority (highest to lowest) + + 1. context_understanding — entity/concept inventories (key actors, + data categories, their roles/relationships), global summaries, + and any structural knowledge that orients the agent for arbitrary + questions + 2. domain_constants — exact numeric values the context defines for + computation: thresholds, rates, formulas, conversion factors, + reference ranges, enum sets, required output field names/types. + These must remain numerically precise — do not abstract them. + 3. context_roadmap — section/chapter/document index with topics and + approximate locations — a Table of Contents the agent won't have + to rebuild + 4. reusable_results — agent-derived aggregated outputs (counts, + distributions, classifications) from processing the full context + that multiple questions would need. Note the computation method + to judge reliability. + 5. parsing_schema — format observations, delimiters, splitting + methods — cheap to rediscover but saves one iteration + + ## Do NOT add + + - Facts that answer only one specific question (verbatim quotes + resolving a single query) + - Raw data dumps or lengthy excerpts copied from the context — + abstract these into higher-level understanding + - Advisory rules, warnings, or meta-instructions ("always do X", + "never do Y") — these consume budget and are not reliably followed + - Verbose passages or long excerpts — prefer compact summaries + + ## Do NOT abstract away + + - Exact numeric values (thresholds, rates, formulas, conversion + factors) that the context defines for computation + - Reference values, enum sets, or allowed value lists + - Output field names, types, and structural requirements + - These are domain constants, not raw data — they must remain precise + + Token-budget enforcement is handled by a separate evictor; focus on + selection quality. But be mindful of the budget when proposing edits. + """ + + diagnosis: str = dspy.InputField(desc="Distiller's narrative diagnosis.") + item_tags: dict[str, ItemTag] = dspy.InputField(desc="Per-item tags from the Distiller.") + cache_candidates: list[CacheCandidate] = dspy.InputField( + desc="Candidate items the Distiller proposed." + ) + current_map: ContextMap = dspy.InputField(desc="Current context map.") + question: str = dspy.InputField(desc="Question the agent was answering.") + token_budget: int = dspy.InputField(desc="Hard token budget for the context map.") + current_tokens: int = dspy.InputField(desc="Current token count of the context map.") + + reasoning: str = dspy.OutputField( + desc="Brief explanation of why these edits improve the shared understanding " + "cached in the context map." + ) + operations: list[Operation] = dspy.OutputField( + desc="Ordered list of ADD/DELETE/REPLACE ops to apply. Empty if nothing " + "is worth changing." + ) + + +class Cartographer(dspy.Module): + """Translates the Distiller's structured reflection into concrete edits + against the context map. + + Owns *what is worth keeping* — selects which tagged items to drop, which + candidates to add, and which existing items to rewrite. Token-budget + enforcement is the Evictor's job. + """ + + def __init__(self): + super().__init__() + self.predict = dspy.Predict(CartographerSig) + + def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, + token_budget, current_tokens): + return self.predict( + diagnosis=diagnosis, + item_tags=item_tags, + cache_candidates=cache_candidates, + current_map=current_map, + question=question, + token_budget=token_budget, + current_tokens=current_tokens, + ) diff --git a/src/codespy/agents/hippocampus/modules/distiller.py b/src/codespy/agents/hippocampus/modules/distiller.py new file mode 100644 index 0000000..64adc07 --- /dev/null +++ b/src/codespy/agents/hippocampus/modules/distiller.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import dspy + +from codespy.agents.hippocampus.context_map import ( + CacheCandidate, + ContextMap, + ItemTag, +) + + +class DistillerSig(dspy.Signature): + """You are an expert analyst reviewing an agent's execution trajectory + after it interacted with a long external context to answer a question. + + The context map prepended to the agent is a compact CACHE OF + UNDERSTANDING about the external context — not answers to specific + questions. It should accumulate structural knowledge that helps with + ANY future question on the same context, the way a human builds a + mental model after reading a document. + + ## Key Principle: Cache Understanding, Not Answers + + The context map captures the agent's evolving understanding of the + context — NOT answers to specific questions. Think of it as the mental + model a human builds after reading a document: structure, key entities, + relationships, and global summaries that help with ANY question about + the content. + + ## Orientation vs. Question-Specific Work + + Observe what the agent spent its iterations doing. Work falls into two + categories: + 1. ORIENTATION WORK — figuring out what the context is, how it's + organized, what entities/concepts exist, how they relate. This + understanding transfers to ANY future question. + 2. QUESTION-SPECIFIC WORK — locating the specific passage or fact + needed for THIS question. This rarely helps other questions. + + Focus on caching category (1). Ask: "If a different, unrelated question + were asked about this same context, would this cached item save the + agent work?" + + ## Produce three outputs + + 1. DIAGNOSIS — Brief analysis of: + - How many iterations the agent spent on orientation vs. + question-specific work + - Whether the agent re-discovered structural information that was + already available (or should have been cached) + - What kind of contextual understanding the agent built that + could transfer to future questions + + 2. ITEM_TAGS — For EVERY item currently in the map, tag it exactly: + - helpful: directly helped or would directly help this run + - harmful: misleading, incorrect, or actively hurts performance + - neutral: correct domain knowledge not relevant to THIS question + but plausibly useful for other questions + - stale: outdated, superseded, or no longer accurate + When tagging, distinguish between "not needed for this question" + (neutral) from "not useful for any question" (harmful/stale). + Domain constants, formulas, and output schemas not exercised this + run are typically NEUTRAL, not harmful. + + 3. CACHE_CANDIDATES — Items to ADD. Value tiers: + + Highest value — structural understanding that transfers across + questions: + - Context structure map: what sections/chapters/documents exist, + their topics, and approximate locations (like a Table of + Contents the agent won't have to rebuild) + - Entity/concept inventory: key characters, actors, concepts, or + data categories and their roles or relationships — a brief + "glossary" that orients the agent + - Domain constants: exact numeric values the context defines — + thresholds, rates, formulas, conversion factors, reference + ranges, enum sets, required output field names/types. Keep + these numerically precise. + - Global summaries: high-level understanding of what the context + is about — genre, time period, key themes, nature of the data + — that frames any question + + Medium value: + - Parsing schema: document delimiters, boundary patterns, field + format, how to reliably split or locate items in the context + - Shared intermediate computations: aggregated results (counts, + distributions, classifications) that the agent derived by + processing the full context and that multiple questions would + need. Note the computation method to judge reliability. + + Do NOT cache: + - Facts that answer only one specific question (e.g., a verbatim + quote that resolves a single query) + - Verbose passages or long excerpts — prefer compact summaries + - Advisory rules, warnings, or meta-instructions ("always do X", + "never do Y") — the cache is for understanding, not instructions + - Results from naive surface-level text operations (e.g., + str.count() for frequency estimation) + - Verbatim answers to the current question + + Do NOT abstract away exact numeric values, enum sets, output field + names/types — these are domain constants and must remain precise. + + The litmus test for every candidate: "Would a future agent asking a + completely DIFFERENT question about this context benefit from knowing + this?" + """ + + trajectory: str = dspy.InputField(desc="The agent's full execution trajectory.") + context_map: ContextMap = dspy.InputField(desc="Current context map (with item IDs).") + question: str = dspy.InputField(desc="The question the agent was answering.") + + diagnosis: str = dspy.OutputField( + desc="Brief analysis of orientation vs. question-specific work, whether " + "structural info was re-discovered that should have been cached, and what " + "transferable understanding the agent built." + ) + item_tags: dict[str, ItemTag] = dspy.OutputField( + desc="Per-item-id tag for EVERY item currently in the context map. " + "Keys must match existing item ids exactly." + ) + cache_candidates: list[CacheCandidate] = dspy.OutputField( + desc="Candidate items to add. Each <= ~80 tokens; structural/transferable only." + ) + + +class Distiller(dspy.Module): + """Extracts transferable orientation knowledge from an agent trajectory. + + The context map is a prompt-resident cache of *understanding*, not + answers. The Distiller separates orientation work (what the context + contains, how it's organized, which constants matter) from question- + specific work, tags every existing item, and proposes new candidates. + """ + + def __init__(self): + super().__init__() + self.predict = dspy.Predict(DistillerSig) + + def forward(self, trajectory: str, context_map: str, question: str): + return self.predict(trajectory=trajectory, context_map=context_map, question=question) From 6bdbafb3275e91c3d50deb27260df9b9a137710d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 23 Jul 2026 15:37:12 +0200 Subject: [PATCH 02/79] improve trajectory truncation --- src/codespy/agents/hippocampus/budget.py | 203 +++++++++++++++---- src/codespy/agents/hippocampus/hypocampus.py | 10 +- 2 files changed, 173 insertions(+), 40 deletions(-) diff --git a/src/codespy/agents/hippocampus/budget.py b/src/codespy/agents/hippocampus/budget.py index cca885e..4f4c0a2 100644 --- a/src/codespy/agents/hippocampus/budget.py +++ b/src/codespy/agents/hippocampus/budget.py @@ -8,9 +8,28 @@ _ENCODING = tiktoken.get_encoding("o200k_base") _MAX_INPUT_FIELD_TOKENS = 256 +# Eviction priority — lower number = evict first +_SECTION_EVICT_PRIORITY: dict[str, int] = { + "parsing_schema": 0, # evict first — cheap to rediscover + "reusable_results": 1, # agent-derived; can be recomputed + "domain_constants": 2, # exact values worth protecting + "context_roadmap": 3, # protected — structural index + "context_understanding": 4, # most protected — core orientation +} + + +# --------------------------------------------------------------------------- +# Token counting +# --------------------------------------------------------------------------- + def count_tokens(s: str) -> int: return len(_ENCODING.encode(s)) + +# --------------------------------------------------------------------------- +# Input serialisation (for the question field sent to the Distiller) +# --------------------------------------------------------------------------- + def format_inputs(kwargs: dict) -> str: """Serialize call inputs (excluding context_map) for the distiller. @@ -36,15 +55,9 @@ def format_inputs(kwargs: dict) -> str: return "\n".join(parts) -# Eviction priority -_SECTION_EVICT_PRIORITY: dict[str, int] = { - "parsing_schema": 0, # evict first — cheap to rediscover - "reusable_results": 1, # agent-derived; can be recomputed - "domain_constants": 2, # exact values worth protecting - "context_roadmap": 3, # protected — structural index - "context_understanding": 4, # most protected — core orientation -} - +# --------------------------------------------------------------------------- +# Eviction +# --------------------------------------------------------------------------- def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: if count_tokens(cmap.render()) <= budget: @@ -70,37 +83,155 @@ def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: return trial return cmap.without(removed) -def format_trajectory(pred: dspy.Prediction) -> str: - traj = getattr(pred, "trajectory", None) - if isinstance(traj, list): - parts = [] - for i, entry in enumerate(traj): - parts.append(f"--- Step {i + 1} ---") - if entry.get("reasoning"): - parts.append(f"Reasoning: {entry['reasoning']}") - parts.append(f"Code:\n{entry['code']}") - parts.append(f"Output:\n{entry['output']}") - return "\n".join(parts) - if isinstance(traj, dict): - return "\n".join(f"{k}: {v}" for k, v in traj.items()) - try: - return "\n".join(f"{k}: {v}" for k, v in pred.toDict().items()) - except Exception: - return str(pred) +# --------------------------------------------------------------------------- +# Trajectory formatting with optional step-aware head+tail bounding +# --------------------------------------------------------------------------- + +def _format_step(i: int, entry: dict) -> str: + parts = [f"--- Step {i + 1} ---"] + if entry.get("reasoning"): + parts.append(f"Reasoning: {entry['reasoning']}") + parts.append(f"Code:\n{entry['code']}") + parts.append(f"Output:\n{entry['output']}") + return "\n".join(parts) -def truncate_trajectory(text: str, max_tokens: int) -> str: - """Keep as many leading steps as fit within max_tokens.""" + +def _head_tail_text(text: str, max_tokens: int, head_ratio: float = 0.6) -> str: + """Keep the first head_ratio and last (1-head_ratio) of the token budget, + dropping the middle with an omission marker. + + Operates at line granularity; returns text unchanged if it fits. + """ if count_tokens(text) <= max_tokens: return text + + head_budget = int(max_tokens * head_ratio) + tail_budget = max_tokens - head_budget + lines = text.splitlines(keepends=True) - kept: list[str] = [] - tokens = 0 + + # Collect head lines + head_lines: list[str] = [] + head_tokens = 0 for line in lines: - line_tokens = count_tokens(line) - if tokens + line_tokens > max_tokens: - kept.append("... (truncated)\n") + lt = count_tokens(line) + if head_tokens + lt > head_budget: + break + head_lines.append(line) + head_tokens += lt + + # Collect tail lines (from the end) + tail_lines: list[str] = [] + tail_tokens = 0 + for line in reversed(lines): + lt = count_tokens(line) + if tail_tokens + lt > tail_budget: break - kept.append(line) - tokens += line_tokens - return "".join(kept) + tail_lines.append(line) + tail_tokens += lt + tail_lines.reverse() + + total = count_tokens(text) + omitted = total - head_tokens - tail_tokens + marker = f"... ({omitted} tokens omitted) ...\n" + + return "".join(head_lines) + marker + "".join(tail_lines) + + +def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> str: + """Serialize a dspy trajectory to text, with optional head+tail bounding. + + Args: + pred: The dspy Prediction returned by the wrapped agent. + max_tokens: If None (default), the full trajectory is returned so the + Distiller can do all compression. If set, step-aware head+tail + bounding is applied: whole steps are kept from both the front and + back of the trajectory (60 % head / 40 % tail), and the middle is + replaced by an omission marker. For dict / fallback trajectories, + the same head+tail logic is applied at line granularity. A single + oversized step's Output block is itself head+tail bounded before + the per-step budget accounting. + + Trajectory shapes handled: + list — ReAct / CodeAct: list of dicts with 'code', 'output', + optional 'reasoning'. Step-aware bounding. + dict — flat key/value dump. Line-granularity bounding. + other — str(pred) or pred.toDict() fallback. Line-granularity bounding. + """ + traj = getattr(pred, "trajectory", None) + + # ----- list path (ReAct / CodeAct) ----- + if isinstance(traj, list): + step_texts = [_format_step(i, entry) for i, entry in enumerate(traj)] + + if max_tokens is None: + return "\n\n".join(step_texts) + + # Check if everything fits as-is + full = "\n\n".join(step_texts) + if count_tokens(full) <= max_tokens: + return full + + head_budget = int(max_tokens * 0.6) + tail_budget = max_tokens - head_budget + + # Cap individual oversized step outputs before budgeting + capped: list[str] = [] + for s in step_texts: + if count_tokens(s) > max_tokens: + s = _head_tail_text(s, max_tokens) + capped.append(s) + + # Greedily keep head steps + head_steps: list[str] = [] + head_tokens = 0 + for s in capped: + t = count_tokens(s) + if head_tokens + t > head_budget: + break + head_steps.append(s) + head_tokens += t + + # Greedily keep tail steps (from the end) + tail_steps: list[str] = [] + tail_tokens = 0 + for s in reversed(capped): + t = count_tokens(s) + if tail_tokens + t > tail_budget: + break + tail_steps.append(s) + tail_tokens += t + tail_steps.reverse() + + # Determine omitted range + n_head = len(head_steps) + n_tail = len(tail_steps) + n_total = len(capped) + n_omitted = n_total - n_head - n_tail + + parts = list(head_steps) + if n_omitted > 0: + first_omitted = n_head + 1 + last_omitted = n_total - n_tail + parts.append( + f"--- Steps {first_omitted}–{last_omitted} omitted ({n_omitted} steps) ---" + ) + parts.extend(tail_steps) + return "\n\n".join(parts) + + # ----- dict path ----- + if isinstance(traj, dict): + text = "\n".join(f"{k}: {v}" for k, v in traj.items()) + if max_tokens is None: + return text + return _head_tail_text(text, max_tokens) + + # ----- fallback ----- + try: + text = "\n".join(f"{k}: {v}" for k, v in pred.toDict().items()) + except Exception: + text = str(pred) + if max_tokens is None: + return text + return _head_tail_text(text, max_tokens) diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 0ab66be..50aaa4c 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -10,7 +10,6 @@ evict, format_inputs, format_trajectory, - truncate_trajectory, ) from codespy.agents.hippocampus.context_map import ContextMap, ItemTag from codespy.agents.hippocampus.modules.cartographer import Cartographer @@ -45,7 +44,7 @@ def __init__( self, module: dspy.Module, token_budget: int = 1024, - max_trajectory_tokens: int = 4096, + max_trajectory_tokens: int | None = None, freeze_after: int | None = None, question_field: str | None = None, ): @@ -53,7 +52,10 @@ def __init__( Args: module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. token_budget: Maximum tokens kept in the context map. - max_trajectory_tokens: Maximum tokens from the trajectory fed to the Distiller. + max_trajectory_tokens: Token budget for the trajectory fed to the Distiller. + None (default) passes the full trajectory so the Distiller does all + compression. If set, step-aware head+tail bounding is applied (60 % + head / 40 % tail), preserving both setup and conclusions. freeze_after: Stop updating the map after this many calls (None = always update). question_field: Name of the input field that carries the task description. If set, only that field is passed to the Distiller as the "question". @@ -117,7 +119,7 @@ def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: question = str(inputs.get(self.question_field, "")) else: question = format_inputs(inputs) - trajectory = truncate_trajectory(format_trajectory(pred), self.max_trajectory_tokens) + trajectory = format_trajectory(pred, self.max_trajectory_tokens) distilled = self.distill( trajectory=trajectory, context_map=self.cmap, From 517343385c0c61254fb47736aedbf926c7c14620 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 23 Jul 2026 16:03:49 +0200 Subject: [PATCH 03/79] hippocampus: fix truncation and remove useless stuff --- src/codespy/agents/hippocampus/__init__.py | 3 +- src/codespy/agents/hippocampus/budget.py | 29 ++++++--------- src/codespy/agents/hippocampus/hypocampus.py | 39 ++++++++++---------- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py index 01a2c31..5356f53 100644 --- a/src/codespy/agents/hippocampus/__init__.py +++ b/src/codespy/agents/hippocampus/__init__.py @@ -9,7 +9,7 @@ ) from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig -from codespy.agents.hippocampus.hypocampus import Hypocampus, StepResult +from codespy.agents.hippocampus.hypocampus import Hypocampus __all__ = [ "CacheCandidate", @@ -24,5 +24,4 @@ "OpType", "Hypocampus", "SectionName", - "StepResult", ] diff --git a/src/codespy/agents/hippocampus/budget.py b/src/codespy/agents/hippocampus/budget.py index 4f4c0a2..2a8565c 100644 --- a/src/codespy/agents/hippocampus/budget.py +++ b/src/codespy/agents/hippocampus/budget.py @@ -6,7 +6,6 @@ from codespy.agents.hippocampus.context_map import ContextMap _ENCODING = tiktoken.get_encoding("o200k_base") -_MAX_INPUT_FIELD_TOKENS = 256 # Eviction priority — lower number = evict first _SECTION_EVICT_PRIORITY: dict[str, int] = { @@ -30,29 +29,23 @@ def count_tokens(s: str) -> int: # Input serialisation (for the question field sent to the Distiller) # --------------------------------------------------------------------------- -def format_inputs(kwargs: dict) -> str: - """Serialize call inputs (excluding context_map) for the distiller. +def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: + """Serialize call inputs (excluding context_map) for the Distiller question. - Each field value is truncated to _MAX_INPUT_FIELD_TOKENS so that large - RLM inputs (documents, file dumps) don't blow up the Distiller context. + All fields are included in full. If max_tokens is set, the joined result is + head+tail bounded via _head_tail_text so both the instruction and any + trailing intent survive. See Hypocampus.max_input_tokens for guidance on + when and how to set a limit. """ parts: list[str] = [] for k, v in kwargs.items(): if k == "context_map": continue - text = str(v) - if count_tokens(text) > _MAX_INPUT_FIELD_TOKENS: - lines, kept, tokens = text.splitlines(keepends=True), [], 0 - for line in lines: - lt = count_tokens(line) - if tokens + lt > _MAX_INPUT_FIELD_TOKENS: - kept.append("... (truncated)") - break - kept.append(line) - tokens += lt - text = "".join(kept) - parts.append(f"{k}: {text}") - return "\n".join(parts) + parts.append(f"{k}: {v}") + text = "\n".join(parts) + if max_tokens is None: + return text + return _head_tail_text(text, max_tokens) # --------------------------------------------------------------------------- diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 50aaa4c..9df343f 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -1,7 +1,6 @@ from __future__ import annotations import copy -from dataclasses import dataclass import dspy @@ -24,14 +23,6 @@ def prepend_context_map(sig): type_=ContextMap, ) -@dataclass -class StepResult: - diagnosis: str - reasoning: str - operations_applied: int - map_text: str - - class Hypocampus(dspy.Module): """Wraps a dspy.Module with a test-time-evolving context map. @@ -45,6 +36,7 @@ def __init__( module: dspy.Module, token_budget: int = 1024, max_trajectory_tokens: int | None = None, + max_input_tokens: int | None = None, freeze_after: int | None = None, question_field: str | None = None, ): @@ -54,8 +46,23 @@ def __init__( token_budget: Maximum tokens kept in the context map. max_trajectory_tokens: Token budget for the trajectory fed to the Distiller. None (default) passes the full trajectory so the Distiller does all - compression. If set, step-aware head+tail bounding is applied (60 % - head / 40 % tail), preserving both setup and conclusions. + compression — Set a limit only as a safety net against runaway trajectories + that would exceed the Distiller model's context window. When set, tie it + to that window: keep it at roughly ≤ 50 % of the context window to leave + room for the Distiller prompt, the current context map, the question, + and the output (e.g. ~8192 for a 128k model, ~4096 for smaller windows). + It should be the dominant share of the Distiller input. Step-aware + head+tail bounding (60 % head / 40 % tail) preserves both setup and + conclusions. + max_input_tokens: Token budget for the serialized inputs used as the + Distiller "question" (only active when question_field is None). + None (default) = unbounded — Only set this when a large input field + (e.g. an RLM document dump) is serialized via the fallback path; in + that case ~1024 is a reasonable value, keeping it well below + max_trajectory_tokens and the model's context window. Head+tail + bounding is applied (60 % head / 40 % tail) so both the leading + instruction and any trailing intent survive. Ignored when + question_field is set. freeze_after: Stop updating the map after this many calls (None = always update). question_field: Name of the input field that carries the task description. If set, only that field is passed to the Distiller as the "question". @@ -86,11 +93,11 @@ def __init__( self.cartograph = Cartographer() self.token_budget = token_budget self.max_trajectory_tokens = max_trajectory_tokens + self.max_input_tokens = max_input_tokens self.freeze_after = freeze_after self.question_field = question_field self.cmap = ContextMap() self.scores: dict[str, int] = {} - self.last_step: StepResult | None = None self._calls = 0 self._frozen = False @@ -118,7 +125,7 @@ def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: if self.question_field is not None: question = str(inputs.get(self.question_field, "")) else: - question = format_inputs(inputs) + question = format_inputs(inputs, self.max_input_tokens) trajectory = format_trajectory(pred, self.max_trajectory_tokens) distilled = self.distill( trajectory=trajectory, @@ -157,9 +164,3 @@ def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: live = self.cmap.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} - self.last_step = StepResult( - diagnosis=distilled.diagnosis, - reasoning=edits.reasoning, - operations_applied=len(ops), - map_text=self.cmap.render(), - ) From b6089de37836c6309d76e2e2308b2133e229d770 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 23 Jul 2026 16:32:06 +0200 Subject: [PATCH 04/79] hippocampus: fix truncation and remove useless stuff --- src/codespy/agents/hippocampus/hypocampus.py | 153 ++++++++++++++----- 1 file changed, 113 insertions(+), 40 deletions(-) diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 9df343f..279626a 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -1,10 +1,12 @@ from __future__ import annotations import copy +from typing import Literal import dspy from codespy.agents.hippocampus.budget import ( + _head_tail_text, count_tokens, evict, format_inputs, @@ -14,6 +16,7 @@ from codespy.agents.hippocampus.modules.cartographer import Cartographer from codespy.agents.hippocampus.modules.distiller import Distiller + def prepend_context_map(sig): return sig.prepend( name="context_map", @@ -23,12 +26,45 @@ def prepend_context_map(sig): type_=ContextMap, ) + class Hypocampus(dspy.Module): - """Wraps a dspy.Module with a test-time-evolving context map. + """Wraps a dspy.Module with a context map that evolves via LLM-driven reflection. + + The context map is prepended to every agent call so the agent starts each run + with accumulated orientation knowledge (structure, entities, constants) about + the external context. After calls, the Distiller extracts transferable + understanding and the Cartographer edits the map — "caching understanding, + not answers." + + ## Update modes + + ``update_mode`` controls *when* reflection (Distiller → Cartographer → evict) + runs: + + - **``per_call``** (default) — reflect after every ``forward()``. The map + improves *within* the same task: call N+1 benefits from call N's insights. + Higher LLM cost (one reflection cycle per call). + + - **``episode``** — ``forward()`` only *uses* the map and *buffers* each call's + trajectory. Reflection runs once when you call ``end_episode()``, consolidating + the whole episode into a single Distiller pass. Use this when you want a cheap, + holistic end-of-task reflection rather than incremental updates. The map is + static during the task (read-only) and updated only for *future* tasks. + + ## Examples + + Online (per-call, default):: + + mem = Hypocampus(agent) + pred = mem(task="summarise section 3") + + Batch reflection (episode):: + + mem = Hypocampus(agent, update_mode="episode") + for task in tasks: + pred = mem(task=task) # uses map; trajectories buffered + mem.end_episode() # single end-of-episode reflection - Prepends a `context_map: ContextMap` input field to every predictor - inside the module, then delegates forward() to it. After each call the - trajectory is distilled and the context map is updated. """ def __init__( @@ -37,7 +73,7 @@ def __init__( token_budget: int = 1024, max_trajectory_tokens: int | None = None, max_input_tokens: int | None = None, - freeze_after: int | None = None, + update_mode: Literal["per_call", "episode"] = "per_call", question_field: str | None = None, ): """ @@ -46,30 +82,33 @@ def __init__( token_budget: Maximum tokens kept in the context map. max_trajectory_tokens: Token budget for the trajectory fed to the Distiller. None (default) passes the full trajectory so the Distiller does all - compression — Set a limit only as a safety net against runaway trajectories - that would exceed the Distiller model's context window. When set, tie it - to that window: keep it at roughly ≤ 50 % of the context window to leave - room for the Distiller prompt, the current context map, the question, - and the output (e.g. ~8192 for a 128k model, ~4096 for smaller windows). - It should be the dominant share of the Distiller input. Step-aware - head+tail bounding (60 % head / 40 % tail) preserves both setup and - conclusions. + compression — recommended for most cases. Set a limit only as a safety + net against runaway trajectories that would exceed the Distiller model's + context window. When set, tie it to that window: keep it at roughly + ≤ 50 % of the context window to leave room for the Distiller prompt, + the current context map, the question, and the output (e.g. ~8192 for + a 128k model, ~4096 for smaller windows). It should be the dominant + share of the Distiller input. Step-aware head+tail bounding + (60 % head / 40 % tail) preserves both setup and conclusions. max_input_tokens: Token budget for the serialized inputs used as the Distiller "question" (only active when question_field is None). - None (default) = unbounded — Only set this when a large input field + None (default) = unbounded — suitable for most cases where inputs + are normal-sized task strings. Only set this when a large input field (e.g. an RLM document dump) is serialized via the fallback path; in that case ~1024 is a reasonable value, keeping it well below max_trajectory_tokens and the model's context window. Head+tail bounding is applied (60 % head / 40 % tail) so both the leading instruction and any trailing intent survive. Ignored when question_field is set. - freeze_after: Stop updating the map after this many calls (None = always update). + update_mode: Controls when reflection (Distiller → Cartographer → evict) + runs. See class docstring for full details. + - "per_call" (default): reflect after every forward(). + - "episode": buffer trajectories; call end_episode() to reflect once. question_field: Name of the input field that carries the task description. If set, only that field is passed to the Distiller as the "question". - If None, all input fields are serialized and truncated automatically — - useful for simple signatures but lossy when inputs are large (e.g. RLM - with document inputs). Set this explicitly whenever one field cleanly - captures the user's intent. + If None, all input fields are serialized and head+tail bounded + (controlled by max_input_tokens). Set this explicitly whenever one + field cleanly captures the user's intent. """ super().__init__() @@ -94,39 +133,74 @@ def __init__( self.token_budget = token_budget self.max_trajectory_tokens = max_trajectory_tokens self.max_input_tokens = max_input_tokens - self.freeze_after = freeze_after + self.update_mode = update_mode self.question_field = question_field self.cmap = ContextMap() self.scores: dict[str, int] = {} - self._calls = 0 - self._frozen = False + self._episode: list[tuple[dspy.Prediction, dict]] = [] @property def current_map_text(self) -> str: return self.cmap.render() - def freeze(self) -> None: - """Stop evolving the context map on subsequent forward() calls.""" - self._frozen = True - - def unfreeze(self) -> None: - self._frozen = False - def forward(self, **kwargs) -> dspy.Prediction: pred = self.agent(context_map=self.cmap, **kwargs) - self._calls += 1 - if not self._frozen and ( - self.freeze_after is None or self._calls <= self.freeze_after - ): - self._update_cache(pred, inputs=kwargs) + if self.update_mode == "per_call": + self.reflect(pred, kwargs) + else: # "episode" + self._episode.append((pred, kwargs)) return pred - def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: - if self.question_field is not None: - question = str(inputs.get(self.question_field, "")) - else: - question = format_inputs(inputs, self.max_input_tokens) + def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: + """Run one distillation cycle over ``pred``'s trajectory and update the map. + + Called automatically in ``per_call`` mode. Call manually when you want + fine-grained control (e.g. selectively reflect on specific calls). + """ + question = self._make_question(inputs) trajectory = format_trajectory(pred, self.max_trajectory_tokens) + self._distill_and_apply(trajectory, question) + + def end_episode(self) -> None: + """Consolidate the buffered episode into the map and clear the buffer. + + Only meaningful in ``episode`` mode. A single Distiller pass sees the + entire episode's trajectories concatenated (head+tail bounded by + ``max_trajectory_tokens``). The question is derived from the first + buffered call. No-op if the buffer is empty. + """ + if not self._episode: + return + # Build combined trajectory: each call separated by a call header. + # Individual trajectories are formatted without per-call bounding so + # the combined head+tail pass governs the final size. + parts = [] + for i, (pred, _) in enumerate(self._episode): + parts.append(f"=== Call {i + 1} ===") + parts.append(format_trajectory(pred)) + combined = "\n\n".join(parts) + if self.max_trajectory_tokens is not None: + combined = _head_tail_text(combined, self.max_trajectory_tokens) + # Derive question from the first buffered call + _, first_inputs = self._episode[0] + question = self._make_question(first_inputs) + self._distill_and_apply(combined, question) + self._episode.clear() + + def reset_episode(self) -> None: + """Discard the buffered episode without reflecting.""" + self._episode.clear() + + # ------------------------------------------------------------------ + # Internals + # ------------------------------------------------------------------ + + def _make_question(self, inputs: dict) -> str: + if self.question_field is not None: + return str(inputs.get(self.question_field, "")) + return format_inputs(inputs, self.max_input_tokens) + + def _distill_and_apply(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, context_map=self.cmap, @@ -163,4 +237,3 @@ def _update_cache(self, pred: dspy.Prediction, inputs: dict) -> None: live = self.cmap.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} - From d5b5c024db93b7a32f2efe40b281c3a88d2b8830 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 23 Jul 2026 17:11:16 +0200 Subject: [PATCH 05/79] hippocampus --- src/codespy/agents/hippocampus/hypocampus.py | 165 ++++++++++--------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 279626a..5293aef 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -1,7 +1,6 @@ from __future__ import annotations import copy -from typing import Literal import dspy @@ -36,35 +35,52 @@ class Hypocampus(dspy.Module): understanding and the Cartographer edits the map — "caching understanding, not answers." - ## Update modes + ## Two independent controls - ``update_mode`` controls *when* reflection (Distiller → Cartographer → evict) - runs: + Reflection behaviour is governed by two orthogonal knobs: - - **``per_call``** (default) — reflect after every ``forward()``. The map - improves *within* the same task: call N+1 benefits from call N's insights. - Higher LLM cost (one reflection cycle per call). + 1. **``max_reflects``** — maximum number of forward() calls that also reflect *online* + (in-episode warm-up). ``None`` (default) = no limit, reflect after every call. + ``0`` = never reflect online. ``N`` = reflect for the first N calls, buffer-only + thereafter. - - **``episode``** — ``forward()`` only *uses* the map and *buffers* each call's - trajectory. Reflection runs once when you call ``end_episode()``, consolidating - the whole episode into a single Distiller pass. Use this when you want a cheap, - holistic end-of-task reflection rather than incremental updates. The map is - static during the task (read-only) and updated only for *future* tasks. + 2. **Calling ``end_episode()``** (or not) — whether to consolidate the buffered + episode into the map at the end. Every call is *always* buffered so + ``end_episode()`` is available regardless of the online setting. - ## Examples - - Online (per-call, default):: + Common patterns:: + # Classic per-call (default) — reflect after every call, no end consolidation mem = Hypocampus(agent) - pred = mem(task="summarise section 3") + pred = mem(task="…") - Batch reflection (episode):: + # Pure batch — no online reflection, one holistic pass at the end + mem = Hypocampus(agent, max_reflects=0) + for task in tasks: + pred = mem(task=task) + mem.end_episode() - mem = Hypocampus(agent, update_mode="episode") + # Hybrid — warm up online for the first 3 calls, then holistic consolidation + mem = Hypocampus(agent, max_reflects=3) for task in tasks: - pred = mem(task=task) # uses map; trajectories buffered - mem.end_episode() # single end-of-episode reflection + pred = mem(task=task) + mem.end_episode() + + # Read-only (map never changes) — pure inference + mem = Hypocampus(agent, max_reflects=0) + pred = mem(task="…") # no end_episode() call + + ## Trajectory bounding (two-stage) + When ``max_trajectory_tokens`` is set: + + - **Stage 1** (per call) — each trajectory is head+tail bounded at ``format_trajectory`` + time. This keeps the buffer lightweight. + - **Stage 2** (``end_episode``) — the joined episode is head+tail bounded again, so + the combined result is guaranteed to fit the budget even if many calls are buffered. + + With ``max_trajectory_tokens=None`` (default) both stages are no-ops and the Distiller + receives the full trajectory. """ def __init__( @@ -73,42 +89,35 @@ def __init__( token_budget: int = 1024, max_trajectory_tokens: int | None = None, max_input_tokens: int | None = None, - update_mode: Literal["per_call", "episode"] = "per_call", + max_reflects: int | None = None, question_field: str | None = None, ): """ Args: module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. token_budget: Maximum tokens kept in the context map. - max_trajectory_tokens: Token budget for the trajectory fed to the Distiller. - None (default) passes the full trajectory so the Distiller does all - compression — recommended for most cases. Set a limit only as a safety - net against runaway trajectories that would exceed the Distiller model's - context window. When set, tie it to that window: keep it at roughly - ≤ 50 % of the context window to leave room for the Distiller prompt, - the current context map, the question, and the output (e.g. ~8192 for - a 128k model, ~4096 for smaller windows). It should be the dominant - share of the Distiller input. Step-aware head+tail bounding - (60 % head / 40 % tail) preserves both setup and conclusions. - max_input_tokens: Token budget for the serialized inputs used as the - Distiller "question" (only active when question_field is None). - None (default) = unbounded — suitable for most cases where inputs - are normal-sized task strings. Only set this when a large input field - (e.g. an RLM document dump) is serialized via the fallback path; in - that case ~1024 is a reasonable value, keeping it well below - max_trajectory_tokens and the model's context window. Head+tail - bounding is applied (60 % head / 40 % tail) so both the leading - instruction and any trailing intent survive. Ignored when - question_field is set. - update_mode: Controls when reflection (Distiller → Cartographer → evict) - runs. See class docstring for full details. - - "per_call" (default): reflect after every forward(). - - "episode": buffer trajectories; call end_episode() to reflect once. - question_field: Name of the input field that carries the task description. - If set, only that field is passed to the Distiller as the "question". - If None, all input fields are serialized and head+tail bounded - (controlled by max_input_tokens). Set this explicitly whenever one - field cleanly captures the user's intent. + max_trajectory_tokens: Token budget for trajectories fed to the Distiller. + None (default) = full trajectory, Distiller does all compression — + recommended for most cases. When set, use ≤ ~50 % of the Distiller + model's context window (e.g. ~8192 for 128k, ~4096 for smaller). + Applied per call (stage 1) and again over the combined episode in + end_episode() (stage 2). Step-aware head+tail bounding (60 % head / + 40 % tail) preserves both setup and conclusions. + max_input_tokens: Token budget for serialized inputs used as the Distiller + "question" (fallback path when question_field is None). None (default) + = unbounded. Set ~1024 only when a large input field (e.g. an RLM + document dump) is serialized; keep it well below max_trajectory_tokens. + Ignored when question_field is set. + max_reflects: Maximum number of forward() calls that also reflect online. + None (default): no limit — reflect after every call (classic online learning). + 0: never reflect online — pure buffering until end_episode(). + N: reflect online for the first N calls, buffer-only afterwards. + Every call is always buffered regardless of this setting, so + end_episode() is always available. + question_field: Name of the input field carrying the task description. + If set, only that field is used as the Distiller "question". + If None, all input fields are serialized (bounded by max_input_tokens). + Set this when one field cleanly captures user intent. """ super().__init__() @@ -133,11 +142,12 @@ def __init__( self.token_budget = token_budget self.max_trajectory_tokens = max_trajectory_tokens self.max_input_tokens = max_input_tokens - self.update_mode = update_mode + self.max_reflects = max_reflects self.question_field = question_field self.cmap = ContextMap() self.scores: dict[str, int] = {} - self._episode: list[tuple[dspy.Prediction, dict]] = [] + self._episode: list[str] = [] # per-call bounded trajectory strings + self._episode_question: str | None = None # derived from first buffered call @property def current_map_text(self) -> str: @@ -145,51 +155,52 @@ def current_map_text(self) -> str: def forward(self, **kwargs) -> dspy.Prediction: pred = self.agent(context_map=self.cmap, **kwargs) - if self.update_mode == "per_call": - self.reflect(pred, kwargs) - else: # "episode" - self._episode.append((pred, kwargs)) + # Format once (stage-1 bounded); reused for both buffering and online reflect. + traj = format_trajectory(pred, self.max_trajectory_tokens) + self._episode.append(traj) + if self._episode_question is None: + self._episode_question = self._make_question(kwargs) + # Online reflection: None = no limit (always); N = for the first N calls. + if (self.max_reflects is None + or len(self._episode) <= self.max_reflects): + self._distill_and_apply(traj, self._make_question(kwargs)) return pred def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: """Run one distillation cycle over ``pred``'s trajectory and update the map. - Called automatically in ``per_call`` mode. Call manually when you want - fine-grained control (e.g. selectively reflect on specific calls). + For manual / selective reflection outside of the automatic per-call flow. + The trajectory is bounded by ``max_trajectory_tokens`` (stage 1). """ - question = self._make_question(inputs) - trajectory = format_trajectory(pred, self.max_trajectory_tokens) - self._distill_and_apply(trajectory, question) + self._distill_and_apply( + format_trajectory(pred, self.max_trajectory_tokens), + self._make_question(inputs), + ) def end_episode(self) -> None: """Consolidate the buffered episode into the map and clear the buffer. - Only meaningful in ``episode`` mode. A single Distiller pass sees the - entire episode's trajectories concatenated (head+tail bounded by - ``max_trajectory_tokens``). The question is derived from the first - buffered call. No-op if the buffer is empty. + A single Distiller pass sees all buffered trajectories joined with + ``=== Call k ===`` headers. If ``max_trajectory_tokens`` is set, the + combined text is head+tail bounded (stage 2) after per-call bounding + (stage 1) already applied at append time. The question is derived from + the first buffered call. No-op if the buffer is empty. """ if not self._episode: return - # Build combined trajectory: each call separated by a call header. - # Individual trajectories are formatted without per-call bounding so - # the combined head+tail pass governs the final size. - parts = [] - for i, (pred, _) in enumerate(self._episode): - parts.append(f"=== Call {i + 1} ===") - parts.append(format_trajectory(pred)) - combined = "\n\n".join(parts) + combined = "\n\n".join( + f"=== Call {i + 1} ===\n{t}" for i, t in enumerate(self._episode) + ) if self.max_trajectory_tokens is not None: combined = _head_tail_text(combined, self.max_trajectory_tokens) - # Derive question from the first buffered call - _, first_inputs = self._episode[0] - question = self._make_question(first_inputs) - self._distill_and_apply(combined, question) + self._distill_and_apply(combined, self._episode_question or "") self._episode.clear() + self._episode_question = None def reset_episode(self) -> None: """Discard the buffered episode without reflecting.""" self._episode.clear() + self._episode_question = None # ------------------------------------------------------------------ # Internals From 0823bde3d26cf60279e2004ecd1038ea40db5791 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 24 Jul 2026 17:21:17 +0200 Subject: [PATCH 06/79] add storage to hippocampus --- src/codespy/agents/hippocampus/__init__.py | 4 +- src/codespy/agents/hippocampus/context_map.py | 27 + src/codespy/agents/hippocampus/hypocampus.py | 55 +- src/codespy/agents/hippocampus/persistence.py | 70 +++ src/codespy/tools/aws/__init__.py | 1 + src/codespy/tools/aws/s3/__init__.py | 23 + src/codespy/tools/aws/s3/client.py | 529 ++++++++++++++++++ src/codespy/tools/aws/s3/models.py | 97 ++++ src/codespy/tools/aws/s3/server.py | 201 +++++++ 9 files changed, 1005 insertions(+), 2 deletions(-) create mode 100644 src/codespy/agents/hippocampus/persistence.py create mode 100644 src/codespy/tools/aws/__init__.py create mode 100644 src/codespy/tools/aws/s3/__init__.py create mode 100644 src/codespy/tools/aws/s3/client.py create mode 100644 src/codespy/tools/aws/s3/models.py create mode 100644 src/codespy/tools/aws/s3/server.py diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py index 5356f53..106513f 100644 --- a/src/codespy/agents/hippocampus/__init__.py +++ b/src/codespy/agents/hippocampus/__init__.py @@ -10,6 +10,7 @@ from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig from codespy.agents.hippocampus.hypocampus import Hypocampus +from codespy.agents.hippocampus.persistence import MapStore __all__ = [ "CacheCandidate", @@ -18,10 +19,11 @@ "ContextMap", "Distiller", "DistillerSig", + "Hypocampus", "Item", "ItemTag", + "MapStore", "Operation", "OpType", - "Hypocampus", "SectionName", ] diff --git a/src/codespy/agents/hippocampus/context_map.py b/src/codespy/agents/hippocampus/context_map.py index 1b5da1b..443e331 100644 --- a/src/codespy/agents/hippocampus/context_map.py +++ b/src/codespy/agents/hippocampus/context_map.py @@ -144,3 +144,30 @@ def without(self, ids: set[str]) -> ContextMap: lst = cm.section(sec) lst[:] = [it for it in lst if it.id not in ids] return cm + + def to_json(self) -> str: + """Serialize the map to a JSON string. + + ``next_id`` is excluded from the output (it is a transient counter). + Use ``from_json()`` to reload — it recomputes ``next_id`` from the + item IDs present in the map so there are no collisions on subsequent + ADD operations. + """ + return self.model_dump_json(indent=2) + + @classmethod + def from_json(cls, text: str) -> ContextMap: + """Deserialize a map from a JSON string produced by ``to_json()``. + + Recomputes ``next_id`` as one past the highest numeric suffix found + in any item ID (e.g. ``cu-00042`` → suffix 42), so the reloaded map + can safely receive further ADD operations without ID collisions. + """ + cm = cls.model_validate_json(text) + max_n = 0 + for it in cm.all_items(): + suffix = it.id.rsplit("-", 1)[-1] + if suffix.isdigit(): + max_n = max(max_n, int(suffix)) + cm.next_id = max_n + 1 + return cm diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 5293aef..b19b67f 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -14,6 +14,9 @@ from codespy.agents.hippocampus.context_map import ContextMap, ItemTag from codespy.agents.hippocampus.modules.cartographer import Cartographer from codespy.agents.hippocampus.modules.distiller import Distiller +from codespy.agents.hippocampus.persistence import MapStore +from codespy.agents.hippocampus.persistence import load_map as _load_map +from codespy.agents.hippocampus.persistence import save_map as _save_map def prepend_context_map(sig): @@ -177,7 +180,11 @@ def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: self._make_question(inputs), ) - def end_episode(self) -> None: + def end_episode( + self, + store: MapStore | None = None, + path: str | None = None, + ) -> None: """Consolidate the buffered episode into the map and clear the buffer. A single Distiller pass sees all buffered trajectories joined with @@ -185,6 +192,19 @@ def end_episode(self) -> None: combined text is head+tail bounded (stage 2) after per-call bounding (stage 1) already applied at append time. The question is derived from the first buffered call. No-op if the buffer is empty. + + If both ``store`` and ``path`` are provided the updated map is + persisted after consolidation. ``store`` may be a ``FileSystem`` or + an ``S3Client`` instance. + + Args: + store: Optional storage backend to persist the map after the + episode (``FileSystem`` or ``S3Client``). + path: Destination path within the store. Required when ``store`` + is set. + + Raises: + IOError: If persistence is requested and the write fails. """ if not self._episode: return @@ -196,6 +216,39 @@ def end_episode(self) -> None: self._distill_and_apply(combined, self._episode_question or "") self._episode.clear() self._episode_question = None + if store is not None and path is not None: + _save_map(store, path, self.cmap) + + def save_map(self, store: MapStore, path: str) -> None: + """Persist the current context map to ``path`` via ``store``. + + Args: + store: Storage backend (``FileSystem`` or ``S3Client``). + path: Destination path within the store. + + Raises: + IOError: If the write fails. + """ + _save_map(store, path, self.cmap) + + def load_map(self, store: MapStore, path: str) -> None: + """Replace the current map with one loaded from ``path`` via ``store``. + + Resets ``scores`` and clears the episode buffer since they belong to + the previous (now discarded) map. + + Args: + store: Storage backend (``FileSystem`` or ``S3Client``). + path: Source path within the store. + + Raises: + FileNotFoundError: If the path does not exist. + IOError: If reading or parsing fails. + """ + self.cmap = _load_map(store, path) + self.scores = {} + self._episode.clear() + self._episode_question = None def reset_episode(self) -> None: """Discard the buffered episode without reflecting.""" diff --git a/src/codespy/agents/hippocampus/persistence.py b/src/codespy/agents/hippocampus/persistence.py new file mode 100644 index 0000000..90e91c7 --- /dev/null +++ b/src/codespy/agents/hippocampus/persistence.py @@ -0,0 +1,70 @@ +"""Persistence helpers for ContextMap — filesystem and S3 backends.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +from codespy.agents.hippocampus.context_map import ContextMap + + +@runtime_checkable +class MapStore(Protocol): + """Structural protocol satisfied by both ``FileSystem`` and ``S3Client``. + + Any object that provides ``read_file(path)`` / ``write_file(path, content)`` + with these signatures can be passed to ``Hypocampus.save_map`` / + ``Hypocampus.load_map``. + """ + + def read_file(self, path: str, **kwargs) -> object: ... # returns object with .content / .error + def write_file(self, path: str, content: str, **kwargs) -> object: ... # returns object with .success + + +def save_map(store: MapStore, path: str, cmap: ContextMap) -> None: + """Serialize ``cmap`` to JSON and write it to ``path`` via ``store``. + + Args: + store: A ``FileSystem`` or ``S3Client`` instance. + path: Destination path (relative to the store's root / bucket). + cmap: The context map to persist. + + Raises: + IOError: If the write operation fails. + """ + result = store.write_file(path, cmap.to_json(), content_type="application/json") + # Both FileSystem and S3Client return an object with a .success attribute. + # FileSystem.write_file returns None; S3Client returns OperationResult. + # Normalise: treat None (filesystem) as success; check .success for S3. + if result is not None and not getattr(result, "success", True): + error = getattr(result, "error", "unknown error") + raise IOError(f"Failed to save context map to {path!r}: {error}") + + +def load_map(store: MapStore, path: str) -> ContextMap: + """Load a context map from ``path`` via ``store``. + + Args: + store: A ``FileSystem`` or ``S3Client`` instance. + path: Source path (relative to the store's root / bucket). + + Returns: + A ``ContextMap`` with ``next_id`` recomputed from loaded item IDs. + + Raises: + FileNotFoundError: If the path does not exist in the store. + IOError: If reading or parsing fails. + """ + result = store.read_file(path) + # Both clients return a result object with .content and .error. + error = getattr(result, "error", None) + if error is not None: + if "not found" in str(error).lower() or "NoSuchKey" in str(error): + raise FileNotFoundError(f"Context map not found at {path!r}: {error}") + raise IOError(f"Failed to load context map from {path!r}: {error}") + content = getattr(result, "content", "") + if not content: + raise IOError(f"Context map at {path!r} is empty") + try: + return ContextMap.from_json(content) + except Exception as exc: + raise IOError(f"Failed to parse context map from {path!r}: {exc}") from exc diff --git a/src/codespy/tools/aws/__init__.py b/src/codespy/tools/aws/__init__.py new file mode 100644 index 0000000..fb0aba7 --- /dev/null +++ b/src/codespy/tools/aws/__init__.py @@ -0,0 +1 @@ +"""AWS tools for interacting with Amazon Web Services.""" diff --git a/src/codespy/tools/aws/s3/__init__.py b/src/codespy/tools/aws/s3/__init__.py new file mode 100644 index 0000000..bffdeb7 --- /dev/null +++ b/src/codespy/tools/aws/s3/__init__.py @@ -0,0 +1,23 @@ +"""S3 tool — filesystem-like access to a single S3 bucket.""" + +from codespy.tools.aws.s3.client import S3Client +from codespy.tools.aws.s3.models import ( + EntryType, + OperationResult, + S3Content, + S3Entry, + S3Info, + S3Listing, + S3TreeNode, +) + +__all__ = [ + "S3Client", + "EntryType", + "S3Info", + "S3Entry", + "S3Listing", + "S3TreeNode", + "S3Content", + "OperationResult", +] diff --git a/src/codespy/tools/aws/s3/client.py b/src/codespy/tools/aws/s3/client.py new file mode 100644 index 0000000..326e984 --- /dev/null +++ b/src/codespy/tools/aws/s3/client.py @@ -0,0 +1,529 @@ +"""S3 client for filesystem-like operations over a single bucket.""" + +import logging + +from codespy.tools.aws.s3.models import ( + EntryType, + OperationResult, + S3Content, + S3Entry, + S3Info, + S3Listing, + S3TreeNode, +) + +logger = logging.getLogger(__name__) + + +class S3Client: + """Client for S3 operations rooted at a single bucket. + + Treats S3 key prefixes as directories and individual object keys as files, + mirroring the FileSystem client interface. All path arguments are relative + to the bucket root (no leading slash needed). + + Authentication uses the standard boto3 credential chain: + env vars (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), ~/.aws/credentials, + IAM instance role, etc. + """ + + def __init__( + self, + bucket: str, + region: str | None = None, + endpoint_url: str | None = None, + ) -> None: + """Initialize the S3 client rooted at a bucket. + + Args: + bucket: S3 bucket name — all operations are scoped to this bucket. + region: AWS region (e.g. 'us-east-1'). Falls back to boto3 defaults. + endpoint_url: Custom endpoint URL for S3-compatible stores (e.g. MinIO). + """ + import boto3 # type: ignore[import-untyped] + + self.bucket = bucket + + kwargs: dict = {} + if region: + kwargs["region_name"] = region + if endpoint_url: + kwargs["endpoint_url"] = endpoint_url + + self._s3 = boto3.client("s3", **kwargs) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _resolve_path(self, path: str) -> str: + """Normalise a relative path, guarding against escaping the bucket root. + + Mirrors FileSystem._resolve_path(): strips leading slashes, collapses + redundant separators, rejects '..' traversal. + + Args: + path: User-supplied file path or directory prefix. + + Returns: + Normalised S3 key string (no leading slash). + + Raises: + ValueError: If path contains '..' components that escape the root. + """ + normalised = path.lstrip("/") + + parts = normalised.split("/") + resolved: list[str] = [] + for part in parts: + if part == "..": + raise ValueError(f"Path escapes bucket root: {path!r}") + if part and part != ".": + resolved.append(part) + + return "/".join(resolved) + + def _file_name(self, path: str) -> str: + """Return the last component of a path (filename or directory name). + + Args: + path: S3 key or prefix. + + Returns: + Last path component. + """ + return path.rstrip("/").rsplit("/", 1)[-1] + + def _file_extension(self, path: str) -> str: + """Return the file extension from a path (without leading dot). + + Args: + path: S3 object key. + + Returns: + Extension string, e.g. 'py', 'json', or '' if none. + """ + name = self._file_name(path) + if "." in name: + return name.rsplit(".", 1)[-1] + return "" + + def _client_error_code(self, exc: Exception) -> str: + """Safely extract the error code from a botocore ClientError.""" + try: + return exc.response["Error"]["Code"] # type: ignore[attr-defined] + except Exception: + return "" + + # ------------------------------------------------------------------ + # Read operations + # ------------------------------------------------------------------ + + def exists(self, path: str = "") -> bool: + """Check whether a file or directory exists in the bucket. + + For files: uses HeadObject. + For directories: lists with the prefix — exists if any entries found. + + Args: + path: Relative file path or directory prefix to check. + + Returns: + True if the path exists. + """ + file_path = self._resolve_path(path) + + if not file_path: + # Bucket root always exists + return True + + # Try as an exact file first + try: + self._s3.head_object(Bucket=self.bucket, Key=file_path) + return True + except Exception as e: + if self._client_error_code(e) not in ("404", "NoSuchKey"): + logger.warning(f"HeadObject error for {file_path!r}: {e}") + + # Try as a directory prefix + dir_prefix = file_path if file_path.endswith("/") else file_path + "/" + try: + resp = self._s3.list_objects_v2( + Bucket=self.bucket, + Prefix=dir_prefix, + MaxKeys=1, + ) + return bool(resp.get("Contents") or resp.get("CommonPrefixes")) + except Exception as e: + logger.warning(f"ListObjectsV2 error for prefix {dir_prefix!r}: {e}") + return False + + def get_info(self, path: str = "") -> S3Info: + """Get metadata about a file or directory. + + Args: + path: Relative file path or directory prefix. + + Returns: + S3Info with metadata. + + Raises: + FileNotFoundError: If the path does not exist. + """ + file_path = self._resolve_path(path) + + if not file_path: + return S3Info( + path=".", + name=self.bucket, + entry_type=EntryType.DIRECTORY, + ) + + # Try as a file + try: + resp = self._s3.head_object(Bucket=self.bucket, Key=file_path) + return S3Info( + path=file_path, + name=self._file_name(file_path), + entry_type=EntryType.FILE, + size=resp.get("ContentLength", 0), + modified_at=resp.get("LastModified"), + extension=self._file_extension(file_path), + etag=resp.get("ETag", "").strip('"'), + storage_class=resp.get("StorageClass", "STANDARD"), + ) + except Exception as e: + if self._client_error_code(e) not in ("404", "NoSuchKey"): + raise + + # Try as a directory prefix + dir_prefix = file_path if file_path.endswith("/") else file_path + "/" + resp = self._s3.list_objects_v2( + Bucket=self.bucket, + Prefix=dir_prefix, + MaxKeys=1, + ) + if resp.get("Contents") or resp.get("CommonPrefixes"): + return S3Info( + path=file_path, + name=self._file_name(file_path), + entry_type=EntryType.DIRECTORY, + ) + + raise FileNotFoundError(f"Path not found in bucket {self.bucket!r}: {file_path!r}") + + def list_directory( + self, + path: str = "", + include_hidden: bool = False, + ) -> S3Listing: + """List files and subdirectories directly under a path (one level deep). + + Uses S3 Delimiter="/" so sub-prefixes are returned as directories. + + Args: + path: Relative directory path to list (empty string = bucket root). + include_hidden: Whether to include entries starting with '.'. + + Returns: + S3Listing with entries sorted: directories first, then files. + """ + dir_path = self._resolve_path(path) + prefix = (dir_path + "/") if dir_path else "" + + entries: list[S3Entry] = [] + total_files = 0 + total_directories = 0 + continuation_token: str | None = None + + while True: + kwargs: dict = { + "Bucket": self.bucket, + "Delimiter": "/", + "Prefix": prefix, + "MaxKeys": 1000, + } + if continuation_token: + kwargs["ContinuationToken"] = continuation_token + + try: + resp = self._s3.list_objects_v2(**kwargs) + except Exception as e: + logger.error(f"Error listing {prefix!r}: {e}") + break + + # CommonPrefixes → sub-directories + for cp in resp.get("CommonPrefixes", []): + sub = cp.get("Prefix", "") + name = sub.rstrip("/").rsplit("/", 1)[-1] + if not include_hidden and name.startswith("."): + continue + entries.append(S3Entry(name=name, entry_type=EntryType.DIRECTORY)) + total_directories += 1 + + # Contents → files (skip placeholder directory key) + for obj in resp.get("Contents", []): + file_key = obj.get("Key", "") + if file_key == prefix: + continue # zero-byte folder placeholder + name = file_key.rsplit("/", 1)[-1] + if not include_hidden and name.startswith("."): + continue + entries.append( + S3Entry( + name=name, + entry_type=EntryType.FILE, + size=obj.get("Size", 0), + ) + ) + total_files += 1 + + if not resp.get("IsTruncated"): + break + continuation_token = resp.get("NextContinuationToken") + + # Directories first, then files — same sort as FileSystem.list_directory + entries.sort(key=lambda e: (e.entry_type == EntryType.FILE, e.name.lower())) + + return S3Listing( + path=dir_path or ".", + entries=entries, + total_files=total_files, + total_directories=total_directories, + ) + + def read_file( + self, + path: str, + max_bytes: int = 100_000, + max_lines: int | None = None, + ) -> S3Content: + """Read a file from S3 as text. + + Mirrors FileSystem.read_file(): utf-8 → latin-1 fallback, byte and line + truncation, returns error in model rather than raising. + + Args: + path: Relative file path. + max_bytes: Maximum bytes to read (default 100 KB). + max_lines: Maximum lines to read (optional). + + Returns: + S3Content with file data. + """ + file_path = self._resolve_path(path) + if not file_path: + return S3Content(path=path, error="Cannot read: path is empty (bucket root)") + + try: + resp = self._s3.get_object(Bucket=self.bucket, Key=file_path) + except Exception as e: + return S3Content(path=file_path, error=f"GetObject failed: {e}") + + size: int = resp.get("ContentLength", 0) + content_type: str = resp.get("ContentType", "") + truncated = False + + try: + raw: bytes = resp["Body"].read(max_bytes + 1) + except Exception as e: + return S3Content( + path=file_path, + error=f"Error reading body: {e}", + size=size, + content_type=content_type, + ) + + if len(raw) > max_bytes: + raw = raw[:max_bytes] + truncated = True + + # Decode: utf-8 first, latin-1 fallback (same as FileSystem.read_file) + try: + content = raw.decode("utf-8") + except UnicodeDecodeError: + try: + content = raw.decode("latin-1") + except Exception: + return S3Content( + path=file_path, + error="Cannot decode file as text (binary content)", + size=size, + content_type=content_type, + ) + + total_lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0) + + if max_lines is not None: + lines = content.split("\n") + if len(lines) > max_lines: + content = "\n".join(lines[:max_lines]) + truncated = True + + return S3Content( + path=file_path, + content=content, + size=size, + lines=total_lines, + truncated=truncated, + content_type=content_type, + ) + + def get_tree( + self, + path: str = "", + max_depth: int = 3, + include_hidden: bool = False, + ) -> S3TreeNode: + """Get a tree representation of a directory in the bucket. + + Args: + path: Relative directory path to start from (empty = bucket root). + max_depth: Maximum recursion depth. + include_hidden: Whether to include entries starting with '.'. + + Returns: + S3TreeNode representing the directory tree. + """ + dir_path = self._resolve_path(path) + name = self._file_name(dir_path) if dir_path else self.bucket + return self._build_tree(dir_path, name, max_depth, include_hidden, 0) + + def _build_tree( + self, + dir_path: str, + name: str, + max_depth: int, + include_hidden: bool, + current_depth: int, + ) -> S3TreeNode: + """Recursively build an S3TreeNode. + + Args: + dir_path: Current directory path (S3 prefix). + name: Display name for this node. + max_depth: Maximum recursion depth. + include_hidden: Include hidden entries. + current_depth: Current recursion depth counter. + + Returns: + S3TreeNode for this directory. + """ + if current_depth >= max_depth: + return S3TreeNode(name=name, entry_type=EntryType.DIRECTORY) + + listing = self.list_directory(dir_path, include_hidden=include_hidden) + children: list[S3TreeNode] = [] + + for entry in listing.entries: + if entry.entry_type == EntryType.DIRECTORY: + child_path = f"{dir_path}/{entry.name}" if dir_path else entry.name + child = self._build_tree( + child_path, + entry.name, + max_depth, + include_hidden, + current_depth + 1, + ) + else: + child = S3TreeNode(name=entry.name, entry_type=EntryType.FILE) + children.append(child) + + return S3TreeNode( + name=name, + entry_type=EntryType.DIRECTORY, + children=children, + ) + + def get_tree_string( + self, + path: str = "", + max_depth: int = 3, + include_hidden: bool = False, + ) -> str: + """Get a string representation of the directory tree. + + Args: + path: Relative directory path to start from. + max_depth: Maximum recursion depth. + include_hidden: Whether to include hidden entries. + + Returns: + String representation of the tree (same style as FileSystem.get_tree_string). + """ + tree = self.get_tree(path, max_depth, include_hidden) + return tree.to_string() + + # ------------------------------------------------------------------ + # Write operations + # ------------------------------------------------------------------ + + def write_file( + self, + path: str, + content: str, + content_type: str = "text/plain", + ) -> OperationResult: + """Write text content to a file in the bucket. + + Args: + path: Relative file path to write to. + content: Text content to write (encoded as UTF-8). + content_type: MIME type for the object (default 'text/plain'). + + Returns: + OperationResult indicating success or failure. + """ + file_path = self._resolve_path(path) + if not file_path: + return OperationResult( + success=False, + path=path, + error="Cannot write: path resolves to bucket root", + ) + + try: + body = content.encode("utf-8") + self._s3.put_object( + Bucket=self.bucket, + Key=file_path, + Body=body, + ContentType=content_type, + ContentLength=len(body), + ) + return OperationResult( + success=True, + path=file_path, + message=f"Written {len(body)} bytes to s3://{self.bucket}/{file_path}", + ) + except Exception as e: + logger.error(f"PutObject failed for {file_path!r}: {e}") + return OperationResult(success=False, path=file_path, error=str(e)) + + def delete_file(self, path: str) -> OperationResult: + """Delete a file from the bucket. + + Args: + path: Relative file path to delete. + + Returns: + OperationResult indicating success or failure. + """ + file_path = self._resolve_path(path) + if not file_path: + return OperationResult( + success=False, + path=path, + error="Cannot delete: path resolves to bucket root", + ) + + try: + self._s3.delete_object(Bucket=self.bucket, Key=file_path) + return OperationResult( + success=True, + path=file_path, + message=f"Deleted s3://{self.bucket}/{file_path}", + ) + except Exception as e: + logger.error(f"DeleteObject failed for {file_path!r}: {e}") + return OperationResult(success=False, path=file_path, error=str(e)) diff --git a/src/codespy/tools/aws/s3/models.py b/src/codespy/tools/aws/s3/models.py new file mode 100644 index 0000000..de55f4a --- /dev/null +++ b/src/codespy/tools/aws/s3/models.py @@ -0,0 +1,97 @@ +"""Data models for S3 operations (filesystem-like).""" + +from datetime import datetime +from enum import Enum + +from pydantic import BaseModel, Field + + +class EntryType(str, Enum): + """Type of S3 entry.""" + + FILE = "file" + DIRECTORY = "directory" + + +class S3Info(BaseModel): + """Information about an S3 object or prefix (analogous to FileInfo).""" + + path: str = Field(description="Key relative to bucket root (prefix)") + name: str = Field(description="Object name (last component of key)") + entry_type: EntryType = Field(description="Type of entry") + size: int = Field(default=0, description="Size in bytes (0 for directories/prefixes)") + modified_at: datetime | None = Field(default=None, description="Last modified time") + extension: str = Field(default="", description="File extension (empty for directories)") + etag: str = Field(default="", description="ETag of the object (empty for directories)") + storage_class: str = Field(default="", description="S3 storage class") + + +class S3Entry(BaseModel): + """Entry in a directory listing (analogous to DirectoryEntry).""" + + name: str = Field(description="Entry name") + entry_type: EntryType = Field(description="Type of entry") + size: int = Field(default=0, description="Size in bytes (0 for directories)") + + +class S3Listing(BaseModel): + """Result of listing an S3 prefix (analogous to DirectoryListing).""" + + path: str = Field(description="Key prefix (directory path)") + entries: list[S3Entry] = Field(default_factory=list, description="Prefix contents") + total_files: int = Field(default=0, description="Number of objects") + total_directories: int = Field(default=0, description="Number of sub-prefixes") + + +class S3TreeNode(BaseModel): + """Node in an S3 prefix tree (analogous to TreeNode).""" + + name: str = Field(description="Entry name") + entry_type: EntryType = Field(description="Type of entry") + children: list["S3TreeNode"] = Field(default_factory=list, description="Child nodes") + + def to_string(self, prefix: str = "", is_last: bool = True) -> str: + """Convert tree node to string representation. + + Args: + prefix: Current line prefix + is_last: Whether this is the last sibling + + Returns: + String representation of the tree + """ + connector = "└── " if is_last else "├── " + icon = "📁 " if self.entry_type == EntryType.DIRECTORY else "📄 " + result = f"{prefix}{connector}{icon}{self.name}\n" + + child_prefix = prefix + (" " if is_last else "│ ") + for i, child in enumerate(self.children): + result += child.to_string(child_prefix, i == len(self.children) - 1) + + return result + + +class S3Content(BaseModel): + """Result of reading an S3 object (analogous to FileContent).""" + + path: str = Field(description="Object key") + content: str = Field(default="", description="Object content as text") + size: int = Field(default=0, description="Total object size in bytes") + lines: int = Field(default=0, description="Total number of lines") + truncated: bool = Field(default=False, description="Whether content was truncated") + content_type: str = Field(default="", description="Content-Type of the object") + error: str | None = Field(default=None, description="Error message if read failed") + + @property + def success(self) -> bool: + """Check if the object was read successfully.""" + return self.error is None + + +class OperationResult(BaseModel): + """Result of a write or delete operation.""" + + success: bool = Field(description="Whether the operation succeeded") + path: str = Field(description="File path (S3 object key) that was operated on") + message: str = Field(default="", description="Human-readable result message") + error: str | None = Field(default=None, description="Error message if operation failed") diff --git a/src/codespy/tools/aws/s3/server.py b/src/codespy/tools/aws/s3/server.py new file mode 100644 index 0000000..93a856d --- /dev/null +++ b/src/codespy/tools/aws/s3/server.py @@ -0,0 +1,201 @@ +"""MCP server for S3 filesystem-like operations.""" + +import logging +import os +import sys +from functools import lru_cache + +from mcp.server.fastmcp import FastMCP + +from codespy.tools.aws.s3.client import S3Client + +logger = logging.getLogger(__name__) + +# Get caller module from environment (set by mcp_utils.py) +_caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") + +mcp = FastMCP("s3") +_client: S3Client | None = None + + +def _get_client() -> S3Client: + """Get the S3Client instance, raising if not initialized.""" + if _client is None: + raise RuntimeError("S3Client not initialized") + return _client + + +# ------------------------------------------------------------------ +# Read tools (cached, like filesystem/server.py) +# ------------------------------------------------------------------ + + +@lru_cache(maxsize=512) +def _file_exists_cached(path: str) -> bool: + """Cached version of exists.""" + return _get_client().exists(path) + + +@mcp.tool() +def file_exists(path: str = "") -> bool: + """Check if a file or directory exists in the bucket. + + Args: + path: Relative path to check (empty = bucket root) + + Returns: + True if the path exists + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> file_exists: s3://{client.bucket}/{path}") + return _file_exists_cached(path) + + +@lru_cache(maxsize=256) +def _get_file_info_cached(path: str) -> tuple: + """Cached version of get_info.""" + result = _get_client().get_info(path) + return tuple(sorted(result.model_dump().items())) + + +@mcp.tool() +def get_file_info(path: str = "") -> dict: + """Get information about a file or directory. + + Args: + path: Relative path (empty = bucket root) + + Returns: + Dict with path, name, entry_type, size, modified_at, extension, etag, storage_class + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> get_file_info: s3://{client.bucket}/{path}") + return dict(_get_file_info_cached(path)) + + +@lru_cache(maxsize=256) +def _list_directory_cached(path: str, include_hidden: bool) -> tuple: + """Cached version of list_directory.""" + result = _get_client().list_directory(path, include_hidden) + return tuple(sorted(result.model_dump().items())) + + +@mcp.tool() +def list_directory(path: str = "", include_hidden: bool = False) -> dict: + """List files and subdirectories directly under a path (one level deep). + + Args: + path: Relative directory path (empty = bucket root) + include_hidden: Whether to include entries starting with '.' + + Returns: + Dict with path, entries, total_files, total_directories + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> list_directory: s3://{client.bucket}/{path}") + return dict(_list_directory_cached(path, include_hidden)) + + +@lru_cache(maxsize=128) +def _get_tree_cached(path: str, max_depth: int, include_hidden: bool) -> str: + """Cached version of get_tree_string.""" + return _get_client().get_tree_string(path, max_depth, include_hidden) + + +@mcp.tool() +def get_tree(path: str = "", max_depth: int = 3, include_hidden: bool = False) -> str: + """Get a string representation of the directory tree. + + Args: + path: Relative directory path (empty = bucket root) + max_depth: Maximum depth to traverse + include_hidden: Whether to include entries starting with '.' + + Returns: + String representation of the directory tree + """ + client = _get_client() + logger.info( + f"[S3] {_caller_module} -> get_tree: s3://{client.bucket}/{path} (depth={max_depth})" + ) + return _get_tree_cached(path, max_depth, include_hidden) + + +@lru_cache(maxsize=256) +def _read_file_cached(path: str, max_bytes: int, max_lines: int | None) -> tuple: + """Cached version of read_file.""" + result = _get_client().read_file(path, max_bytes, max_lines) + return tuple(sorted(result.model_dump().items())) + + +@mcp.tool() +def read_file(path: str, max_bytes: int = 100_000, max_lines: int | None = None) -> dict: + """Read a file from the bucket as text. + + Args: + path: Relative file path + max_bytes: Maximum bytes to read (default 100 KB) + max_lines: Maximum lines to read (optional) + + Returns: + Dict with path, content, size, lines, truncated, content_type, error (if any) + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> read_file: s3://{client.bucket}/{path}") + return dict(_read_file_cached(path, max_bytes, max_lines)) + + +# ------------------------------------------------------------------ +# Write tools (not cached) +# ------------------------------------------------------------------ + + +@mcp.tool() +def write_file(path: str, content: str, content_type: str = "text/plain") -> dict: + """Write text content to a file in the bucket. + + Args: + path: Relative file path to write to + content: Text content to write (encoded as UTF-8) + content_type: MIME type for the object (default 'text/plain') + + Returns: + Dict with success, path, message, error (if any) + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> write_file: s3://{client.bucket}/{path}") + result = client.write_file(path, content, content_type) + return result.model_dump() + + +@mcp.tool() +def delete_file(path: str) -> dict: + """Delete a file from the bucket. + + Args: + path: Relative file path to delete + + Returns: + Dict with success, path, message, error (if any) + """ + client = _get_client() + logger.info(f"[S3] {_caller_module} -> delete_file: s3://{client.bucket}/{path}") + result = client.delete_file(path) + return result.model_dump() + + +if __name__ == "__main__": + # Suppress noisy MCP server "Processing request" logs + logging.getLogger("mcp.server").setLevel(logging.WARNING) + logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) + + if len(sys.argv) < 2: + print("Usage: python server.py [region] [endpoint_url]", file=sys.stderr) + sys.exit(1) + + bucket = sys.argv[1] + region = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("AWS_DEFAULT_REGION") + endpoint_url = sys.argv[3] if len(sys.argv) > 3 else os.environ.get("S3_ENDPOINT_URL") + + _client = S3Client(bucket=bucket, region=region, endpoint_url=endpoint_url) + mcp.run() From d53979fa456fde835a1249bb348e1453611b6341 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 24 Jul 2026 17:46:28 +0200 Subject: [PATCH 07/79] add storage to hippocampus --- src/codespy/agents/hippocampus/__init__.py | 2 - src/codespy/agents/hippocampus/hypocampus.py | 10 +- src/codespy/agents/hippocampus/persistence.py | 40 +-- .../agents/reviewer/modules/code_reviewer.py | 2 +- .../agents/reviewer/modules/doc_extractor.py | 3 +- .../reviewer/modules/scope_identifier.py | 2 +- .../reviewer/modules/supply_chain_auditor.py | 2 +- src/codespy/tools/__init__.py | 4 +- src/codespy/tools/aws/__init__.py | 1 - src/codespy/tools/aws/s3/__init__.py | 23 -- src/codespy/tools/aws/s3/models.py | 97 ------- src/codespy/tools/filesystem/__init__.py | 21 -- src/codespy/tools/storage/__init__.py | 27 ++ src/codespy/tools/storage/base.py | 162 ++++++++++++ .../tools/storage/filesystem/__init__.py | 5 + .../tools/{ => storage}/filesystem/client.py | 240 ++++++++---------- .../tools/{ => storage}/filesystem/server.py | 4 +- .../tools/{filesystem => storage}/models.py | 61 +++-- src/codespy/tools/storage/s3/__init__.py | 5 + .../tools/{aws => storage}/s3/client.py | 238 +++-------------- .../tools/{aws => storage}/s3/server.py | 2 +- 21 files changed, 410 insertions(+), 541 deletions(-) delete mode 100644 src/codespy/tools/aws/__init__.py delete mode 100644 src/codespy/tools/aws/s3/__init__.py delete mode 100644 src/codespy/tools/aws/s3/models.py delete mode 100644 src/codespy/tools/filesystem/__init__.py create mode 100644 src/codespy/tools/storage/__init__.py create mode 100644 src/codespy/tools/storage/base.py create mode 100644 src/codespy/tools/storage/filesystem/__init__.py rename src/codespy/tools/{ => storage}/filesystem/client.py (58%) rename src/codespy/tools/{ => storage}/filesystem/server.py (98%) rename src/codespy/tools/{filesystem => storage}/models.py (59%) create mode 100644 src/codespy/tools/storage/s3/__init__.py rename src/codespy/tools/{aws => storage}/s3/client.py (63%) rename src/codespy/tools/{aws => storage}/s3/server.py (99%) diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py index 106513f..b2e1f13 100644 --- a/src/codespy/agents/hippocampus/__init__.py +++ b/src/codespy/agents/hippocampus/__init__.py @@ -10,7 +10,6 @@ from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig from codespy.agents.hippocampus.hypocampus import Hypocampus -from codespy.agents.hippocampus.persistence import MapStore __all__ = [ "CacheCandidate", @@ -22,7 +21,6 @@ "Hypocampus", "Item", "ItemTag", - "MapStore", "Operation", "OpType", "SectionName", diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index b19b67f..fd3bb6f 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -14,9 +14,9 @@ from codespy.agents.hippocampus.context_map import ContextMap, ItemTag from codespy.agents.hippocampus.modules.cartographer import Cartographer from codespy.agents.hippocampus.modules.distiller import Distiller -from codespy.agents.hippocampus.persistence import MapStore from codespy.agents.hippocampus.persistence import load_map as _load_map from codespy.agents.hippocampus.persistence import save_map as _save_map +from codespy.tools.storage.base import Storage def prepend_context_map(sig): @@ -182,7 +182,7 @@ def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: def end_episode( self, - store: MapStore | None = None, + store: Storage | None = None, path: str | None = None, ) -> None: """Consolidate the buffered episode into the map and clear the buffer. @@ -198,7 +198,7 @@ def end_episode( an ``S3Client`` instance. Args: - store: Optional storage backend to persist the map after the + store: Optional ``Storage`` backend to persist the map after the episode (``FileSystem`` or ``S3Client``). path: Destination path within the store. Required when ``store`` is set. @@ -219,7 +219,7 @@ def end_episode( if store is not None and path is not None: _save_map(store, path, self.cmap) - def save_map(self, store: MapStore, path: str) -> None: + def save_map(self, store: Storage, path: str) -> None: """Persist the current context map to ``path`` via ``store``. Args: @@ -231,7 +231,7 @@ def save_map(self, store: MapStore, path: str) -> None: """ _save_map(store, path, self.cmap) - def load_map(self, store: MapStore, path: str) -> None: + def load_map(self, store: Storage, path: str) -> None: """Replace the current map with one loaded from ``path`` via ``store``. Resets ``scores`` and clears the episode buffer since they belong to diff --git a/src/codespy/agents/hippocampus/persistence.py b/src/codespy/agents/hippocampus/persistence.py index 90e91c7..8b3e01a 100644 --- a/src/codespy/agents/hippocampus/persistence.py +++ b/src/codespy/agents/hippocampus/persistence.py @@ -2,25 +2,11 @@ from __future__ import annotations -from typing import Protocol, runtime_checkable - from codespy.agents.hippocampus.context_map import ContextMap +from codespy.tools.storage.base import Storage -@runtime_checkable -class MapStore(Protocol): - """Structural protocol satisfied by both ``FileSystem`` and ``S3Client``. - - Any object that provides ``read_file(path)`` / ``write_file(path, content)`` - with these signatures can be passed to ``Hypocampus.save_map`` / - ``Hypocampus.load_map``. - """ - - def read_file(self, path: str, **kwargs) -> object: ... # returns object with .content / .error - def write_file(self, path: str, content: str, **kwargs) -> object: ... # returns object with .success - - -def save_map(store: MapStore, path: str, cmap: ContextMap) -> None: +def save_map(store: Storage, path: str, cmap: ContextMap) -> None: """Serialize ``cmap`` to JSON and write it to ``path`` via ``store``. Args: @@ -32,15 +18,11 @@ def save_map(store: MapStore, path: str, cmap: ContextMap) -> None: IOError: If the write operation fails. """ result = store.write_file(path, cmap.to_json(), content_type="application/json") - # Both FileSystem and S3Client return an object with a .success attribute. - # FileSystem.write_file returns None; S3Client returns OperationResult. - # Normalise: treat None (filesystem) as success; check .success for S3. - if result is not None and not getattr(result, "success", True): - error = getattr(result, "error", "unknown error") - raise IOError(f"Failed to save context map to {path!r}: {error}") + if not result.success: + raise IOError(f"Failed to save context map to {path!r}: {result.error}") -def load_map(store: MapStore, path: str) -> ContextMap: +def load_map(store: Storage, path: str) -> ContextMap: """Load a context map from ``path`` via ``store``. Args: @@ -55,16 +37,14 @@ def load_map(store: MapStore, path: str) -> ContextMap: IOError: If reading or parsing fails. """ result = store.read_file(path) - # Both clients return a result object with .content and .error. - error = getattr(result, "error", None) - if error is not None: - if "not found" in str(error).lower() or "NoSuchKey" in str(error): + if not result.success: + error = result.error or "" + if "not found" in error.lower() or "NoSuchKey" in error: raise FileNotFoundError(f"Context map not found at {path!r}: {error}") raise IOError(f"Failed to load context map from {path!r}: {error}") - content = getattr(result, "content", "") - if not content: + if not result.content: raise IOError(f"Context map at {path!r} is empty") try: - return ContextMap.from_json(content) + return ContextMap.from_json(result.content) except Exception as exc: raise IOError(f"Failed to parse context map from {path!r}: {exc}") from exc diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 337ccce..237acbc 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -165,7 +165,7 @@ async def _create_tools( caller = "code_reviewer" tools.extend(await connect_mcp_server( - tools_dir / "filesystem" / "server.py", + tools_dir / "storage" / "filesystem" / "server.py", [scope_root_str], contexts, caller, )) tools.extend(await connect_mcp_server( diff --git a/src/codespy/agents/reviewer/modules/doc_extractor.py b/src/codespy/agents/reviewer/modules/doc_extractor.py index fd4640e..b87cb12 100644 --- a/src/codespy/agents/reviewer/modules/doc_extractor.py +++ b/src/codespy/agents/reviewer/modules/doc_extractor.py @@ -4,8 +4,7 @@ import re from pathlib import Path -from codespy.tools.filesystem.client import FileSystem -from codespy.tools.filesystem.models import EntryType, TreeNode +from codespy.tools.storage import EntryType, FileSystem, TreeNode logger = logging.getLogger(__name__) diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 860fbd2..d3314c3 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -187,7 +187,7 @@ async def _create_mcp_tools(self, repo_path: Path, is_local: bool = False) -> tu tools_dir = Path(__file__).parent.parent.parent.parent / "tools" repo_path_str = str(repo_path) caller = "scope_identifier" - tools.extend(await connect_mcp_server(tools_dir / "filesystem" / "server.py", [repo_path_str], contexts, caller)) + tools.extend(await connect_mcp_server(tools_dir / "storage" / "filesystem" / "server.py", [repo_path_str], contexts, caller)) tools.extend(await connect_mcp_server(tools_dir / "parsers" / "ripgrep" / "server.py", [repo_path_str], contexts, caller)) tools.extend(await connect_mcp_server(tools_dir / "parsers" / "treesitter" / "server.py", [repo_path_str], contexts, caller)) if not is_local: diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index e86caee..3e5a56d 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -183,7 +183,7 @@ async def _create_scoped_tools( # Add filesystem tools for reading files and exploring structure tools.extend(await connect_mcp_server( - tools_dir / "filesystem" / "server.py", + tools_dir / "storage" / "filesystem" / "server.py", [scope_root_str], contexts, caller, diff --git a/src/codespy/tools/__init__.py b/src/codespy/tools/__init__.py index 56935cc..fa7f013 100644 --- a/src/codespy/tools/__init__.py +++ b/src/codespy/tools/__init__.py @@ -1,7 +1,7 @@ """Tools for code parsing, Git platform integration, filesystem operations, web browsing, and security scanning.""" from codespy.tools.cyber import OSVClient, ScanResult, ScanSummary, Vulnerability -from codespy.tools.filesystem import FileSystem +from codespy.tools.storage import FileSystem, S3Client, Storage from codespy.tools.git import ( ChangedFile, GitClient, @@ -17,6 +17,8 @@ __all__ = [ "FileSystem", + "S3Client", + "Storage", "GitClient", "get_client", "detect_platform", diff --git a/src/codespy/tools/aws/__init__.py b/src/codespy/tools/aws/__init__.py deleted file mode 100644 index fb0aba7..0000000 --- a/src/codespy/tools/aws/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""AWS tools for interacting with Amazon Web Services.""" diff --git a/src/codespy/tools/aws/s3/__init__.py b/src/codespy/tools/aws/s3/__init__.py deleted file mode 100644 index bffdeb7..0000000 --- a/src/codespy/tools/aws/s3/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""S3 tool — filesystem-like access to a single S3 bucket.""" - -from codespy.tools.aws.s3.client import S3Client -from codespy.tools.aws.s3.models import ( - EntryType, - OperationResult, - S3Content, - S3Entry, - S3Info, - S3Listing, - S3TreeNode, -) - -__all__ = [ - "S3Client", - "EntryType", - "S3Info", - "S3Entry", - "S3Listing", - "S3TreeNode", - "S3Content", - "OperationResult", -] diff --git a/src/codespy/tools/aws/s3/models.py b/src/codespy/tools/aws/s3/models.py deleted file mode 100644 index de55f4a..0000000 --- a/src/codespy/tools/aws/s3/models.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Data models for S3 operations (filesystem-like).""" - -from datetime import datetime -from enum import Enum - -from pydantic import BaseModel, Field - - -class EntryType(str, Enum): - """Type of S3 entry.""" - - FILE = "file" - DIRECTORY = "directory" - - -class S3Info(BaseModel): - """Information about an S3 object or prefix (analogous to FileInfo).""" - - path: str = Field(description="Key relative to bucket root (prefix)") - name: str = Field(description="Object name (last component of key)") - entry_type: EntryType = Field(description="Type of entry") - size: int = Field(default=0, description="Size in bytes (0 for directories/prefixes)") - modified_at: datetime | None = Field(default=None, description="Last modified time") - extension: str = Field(default="", description="File extension (empty for directories)") - etag: str = Field(default="", description="ETag of the object (empty for directories)") - storage_class: str = Field(default="", description="S3 storage class") - - -class S3Entry(BaseModel): - """Entry in a directory listing (analogous to DirectoryEntry).""" - - name: str = Field(description="Entry name") - entry_type: EntryType = Field(description="Type of entry") - size: int = Field(default=0, description="Size in bytes (0 for directories)") - - -class S3Listing(BaseModel): - """Result of listing an S3 prefix (analogous to DirectoryListing).""" - - path: str = Field(description="Key prefix (directory path)") - entries: list[S3Entry] = Field(default_factory=list, description="Prefix contents") - total_files: int = Field(default=0, description="Number of objects") - total_directories: int = Field(default=0, description="Number of sub-prefixes") - - -class S3TreeNode(BaseModel): - """Node in an S3 prefix tree (analogous to TreeNode).""" - - name: str = Field(description="Entry name") - entry_type: EntryType = Field(description="Type of entry") - children: list["S3TreeNode"] = Field(default_factory=list, description="Child nodes") - - def to_string(self, prefix: str = "", is_last: bool = True) -> str: - """Convert tree node to string representation. - - Args: - prefix: Current line prefix - is_last: Whether this is the last sibling - - Returns: - String representation of the tree - """ - connector = "└── " if is_last else "├── " - icon = "📁 " if self.entry_type == EntryType.DIRECTORY else "📄 " - result = f"{prefix}{connector}{icon}{self.name}\n" - - child_prefix = prefix + (" " if is_last else "│ ") - for i, child in enumerate(self.children): - result += child.to_string(child_prefix, i == len(self.children) - 1) - - return result - - -class S3Content(BaseModel): - """Result of reading an S3 object (analogous to FileContent).""" - - path: str = Field(description="Object key") - content: str = Field(default="", description="Object content as text") - size: int = Field(default=0, description="Total object size in bytes") - lines: int = Field(default=0, description="Total number of lines") - truncated: bool = Field(default=False, description="Whether content was truncated") - content_type: str = Field(default="", description="Content-Type of the object") - error: str | None = Field(default=None, description="Error message if read failed") - - @property - def success(self) -> bool: - """Check if the object was read successfully.""" - return self.error is None - - -class OperationResult(BaseModel): - """Result of a write or delete operation.""" - - success: bool = Field(description="Whether the operation succeeded") - path: str = Field(description="File path (S3 object key) that was operated on") - message: str = Field(default="", description="Human-readable result message") - error: str | None = Field(default=None, description="Error message if operation failed") diff --git a/src/codespy/tools/filesystem/__init__.py b/src/codespy/tools/filesystem/__init__.py deleted file mode 100644 index 50fd010..0000000 --- a/src/codespy/tools/filesystem/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -"""FileSystem module for file operations.""" - -from codespy.tools.filesystem.client import FileSystem -from codespy.tools.filesystem.models import ( - DirectoryEntry, - DirectoryListing, - EntryType, - FileContent, - FileInfo, - TreeNode, -) - -__all__ = [ - "FileSystem", - "DirectoryEntry", - "DirectoryListing", - "EntryType", - "FileContent", - "FileInfo", - "TreeNode", -] \ No newline at end of file diff --git a/src/codespy/tools/storage/__init__.py b/src/codespy/tools/storage/__init__.py new file mode 100644 index 0000000..d39c1e0 --- /dev/null +++ b/src/codespy/tools/storage/__init__.py @@ -0,0 +1,27 @@ +"""Unified storage — local filesystem and S3 backends with a shared interface.""" + +from codespy.tools.storage.base import Storage +from codespy.tools.storage.filesystem.client import FileSystem +from codespy.tools.storage.models import ( + Content, + Entry, + EntryType, + Info, + Listing, + OperationResult, + TreeNode, +) +from codespy.tools.storage.s3.client import S3Client + +__all__ = [ + "Content", + "Entry", + "EntryType", + "FileSystem", + "Info", + "Listing", + "OperationResult", + "S3Client", + "Storage", + "TreeNode", +] diff --git a/src/codespy/tools/storage/base.py b/src/codespy/tools/storage/base.py new file mode 100644 index 0000000..14ff3af --- /dev/null +++ b/src/codespy/tools/storage/base.py @@ -0,0 +1,162 @@ +"""Abstract base class defining the unified storage interface.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from codespy.tools.storage.models import ( + Content, + Entry, + Info, + Listing, + OperationResult, + TreeNode, +) + + +class Storage(ABC): + """Abstract storage backend — implemented by both FileSystem and S3Client. + + Provides a uniform interface for reading, writing, and navigating file-like + storage, whether backed by the local filesystem or an S3 bucket. + """ + + # ------------------------------------------------------------------ + # Read operations + # ------------------------------------------------------------------ + + @abstractmethod + def exists(self, path: str = "") -> bool: + """Check whether a path exists. + + Args: + path: Relative path to check. + + Returns: + True if the path exists. + """ + ... + + @abstractmethod + def get_info(self, path: str = "") -> Info: + """Get metadata about a file or directory. + + Args: + path: Relative path. + + Returns: + Info with metadata. + + Raises: + FileNotFoundError: If the path does not exist. + """ + ... + + @abstractmethod + def list_directory( + self, + path: str = "", + include_hidden: bool = False, + ) -> Listing: + """List the contents of a directory (one level deep). + + Args: + path: Relative directory path. + include_hidden: Whether to include hidden entries. + + Returns: + Listing with entries. + """ + ... + + @abstractmethod + def read_file( + self, + path: str, + max_bytes: int = 100_000, + max_lines: int | None = None, + ) -> Content: + """Read a file as text. + + Args: + path: Relative file path. + max_bytes: Maximum bytes to read. + max_lines: Maximum lines to read. + + Returns: + Content with file data (error field set on failure). + """ + ... + + @abstractmethod + def get_tree( + self, + path: str = "", + max_depth: int = 3, + include_hidden: bool = False, + ) -> TreeNode: + """Get a tree representation of a directory. + + Args: + path: Relative directory path. + max_depth: Maximum recursion depth. + include_hidden: Whether to include hidden entries. + + Returns: + TreeNode representing the directory tree. + """ + ... + + def get_tree_string( + self, + path: str = "", + max_depth: int = 3, + include_hidden: bool = False, + ) -> str: + """Get a string representation of the directory tree. + + Args: + path: Relative directory path. + max_depth: Maximum recursion depth. + include_hidden: Whether to include hidden entries. + + Returns: + String representation of the tree. + """ + tree = self.get_tree(path, max_depth, include_hidden) + return tree.to_string() + + # ------------------------------------------------------------------ + # Write operations + # ------------------------------------------------------------------ + + @abstractmethod + def write_file( + self, + path: str, + content: str, + content_type: str = "text/plain", + ) -> OperationResult: + """Write text content to a file. + + Args: + path: Relative file path. + content: Text content to write (UTF-8 encoded). + content_type: MIME type hint (used by S3; ignored by filesystem). + + Returns: + OperationResult indicating success or failure. + """ + ... + + @abstractmethod + def delete_file(self, path: str) -> OperationResult: + """Delete a file. + + Args: + path: Relative file path to delete. + + Returns: + OperationResult indicating success or failure. + """ + ... diff --git a/src/codespy/tools/storage/filesystem/__init__.py b/src/codespy/tools/storage/filesystem/__init__.py new file mode 100644 index 0000000..17aec6c --- /dev/null +++ b/src/codespy/tools/storage/filesystem/__init__.py @@ -0,0 +1,5 @@ +"""Local filesystem storage backend.""" + +from codespy.tools.storage.filesystem.client import FileSystem + +__all__ = ["FileSystem"] diff --git a/src/codespy/tools/filesystem/client.py b/src/codespy/tools/storage/filesystem/client.py similarity index 58% rename from src/codespy/tools/filesystem/client.py rename to src/codespy/tools/storage/filesystem/client.py index 1f48a23..4e99cfc 100644 --- a/src/codespy/tools/filesystem/client.py +++ b/src/codespy/tools/storage/filesystem/client.py @@ -1,22 +1,26 @@ -"""FileSystem client for file operations.""" +"""FileSystem client for local filesystem operations.""" + +from __future__ import annotations import logging from pathlib import Path -from codespy.tools.filesystem.models import ( - DirectoryEntry, - DirectoryListing, +from codespy.tools.storage.base import Storage +from codespy.tools.storage.models import ( + Content, + Entry, EntryType, - FileContent, - FileInfo, + Info, + Listing, + OperationResult, TreeNode, ) logger = logging.getLogger(__name__) -class FileSystem: - """Client for filesystem operations. +class FileSystem(Storage): + """Local filesystem storage client. Provides secure file operations restricted to a root directory. """ @@ -41,8 +45,8 @@ def __init__(self, root: str | Path, create_if_missing: bool = True) -> None: """Initialize the filesystem client. Args: - root: Root directory for all operations - create_if_missing: Create the root directory if it doesn't exist + root: Root directory for all operations. + create_if_missing: Create the root directory if it doesn't exist. """ self.root = Path(root).resolve() if not self.root.exists(): @@ -58,20 +62,19 @@ def _resolve_path(self, path: str) -> Path: """Resolve a path relative to root, with security checks. Args: - path: Relative path + path: Relative path. Returns: - Absolute path + Absolute path. Raises: - ValueError: If path escapes root directory + ValueError: If path escapes root directory. """ if not path or path == ".": return self.root resolved = (self.root / path).resolve() - # Security check: ensure path is within root try: resolved.relative_to(self.root) except ValueError: @@ -79,57 +82,31 @@ def _resolve_path(self, path: str) -> Path: return resolved - def exists(self, path: str = "") -> bool: - """Check if a path exists. + # ------------------------------------------------------------------ + # Read operations + # ------------------------------------------------------------------ - Args: - path: Relative path to check - - Returns: - True if path exists - """ + def exists(self, path: str = "") -> bool: + """Check if a path exists.""" try: resolved = self._resolve_path(path) return resolved.exists() except ValueError: return False - def get_info(self, path: str = "") -> FileInfo: - """Get information about a file or directory. - - Args: - path: Relative path - - Returns: - FileInfo with metadata - - Raises: - FileNotFoundError: If path does not exist - """ + def get_info(self, path: str = "") -> Info: + """Get information about a file or directory.""" resolved = self._resolve_path(path) if not resolved.exists(): raise FileNotFoundError(f"Path not found: {path}") - - return FileInfo.from_path(resolved, self.root) + return Info.from_path(resolved, self.root) def list_directory( self, path: str = "", include_hidden: bool = False, - ) -> DirectoryListing: - """List contents of a directory. - - Args: - path: Relative path to directory - include_hidden: Whether to include hidden files (starting with .) - - Returns: - DirectoryListing with entries - - Raises: - FileNotFoundError: If path does not exist - NotADirectoryError: If path is not a directory - """ + ) -> Listing: + """List contents of a directory.""" resolved = self._resolve_path(path) if not resolved.exists(): @@ -137,13 +114,12 @@ def list_directory( if not resolved.is_dir(): raise NotADirectoryError(f"Not a directory: {path}") - entries: list[DirectoryEntry] = [] + entries: list[Entry] = [] total_files = 0 total_directories = 0 try: for entry in sorted(resolved.iterdir(), key=lambda x: (x.is_file(), x.name.lower())): - # Skip hidden files unless requested if not include_hidden and entry.name.startswith("."): continue @@ -158,19 +134,13 @@ def list_directory( size = entry.stat().st_size if entry_type == EntryType.FILE else 0 - entries.append( - DirectoryEntry( - name=entry.name, - entry_type=entry_type, - size=size, - ) - ) + entries.append(Entry(name=entry.name, entry_type=entry_type, size=size)) except PermissionError as e: logger.warning(f"Permission denied listing {path}: {e}") rel_path = str(resolved.relative_to(self.root)) if resolved != self.root else "." - return DirectoryListing( + return Listing( path=rel_path, entries=entries, total_files=total_files, @@ -182,27 +152,14 @@ def read_file( path: str, max_bytes: int = 100_000, max_lines: int | None = None, - ) -> FileContent: - """Read contents of a file. - - Args: - path: Relative path to file - max_bytes: Maximum bytes to read (default 100KB) - max_lines: Maximum lines to read (optional) - - Returns: - FileContent with file data - - Raises: - FileNotFoundError: If file does not exist - IsADirectoryError: If path is a directory - """ + ) -> Content: + """Read contents of a file.""" resolved = self._resolve_path(path) if not resolved.exists(): - raise FileNotFoundError(f"File not found: {path}") + return Content(path=path, error=f"File not found: {path}") if resolved.is_dir(): - raise IsADirectoryError(f"Cannot read directory: {path}") + return Content(path=path, error=f"Cannot read directory: {path}") file_size = resolved.stat().st_size truncated = False @@ -210,20 +167,17 @@ def read_file( try: content = resolved.read_text(encoding="utf-8") except UnicodeDecodeError: - # Try with latin-1 for binary-ish files try: content = resolved.read_text(encoding="latin-1") except Exception: - raise ValueError(f"Cannot read file as text: {path}") + return Content(path=path, error=f"Cannot read file as text: {path}", size=file_size) total_lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0) - # Truncate by bytes if len(content) > max_bytes: content = content[:max_bytes] truncated = True - # Truncate by lines if max_lines is not None: lines = content.split("\n") if len(lines) > max_lines: @@ -232,7 +186,7 @@ def read_file( rel_path = str(resolved.relative_to(self.root)) - return FileContent( + return Content( path=rel_path, content=content, size=file_size, @@ -246,20 +200,7 @@ def get_tree( max_depth: int = 3, include_hidden: bool = False, ) -> TreeNode: - """Get a tree representation of a directory. - - Args: - path: Relative path to directory - max_depth: Maximum depth to traverse - include_hidden: Whether to include hidden files - - Returns: - TreeNode representing the directory structure - - Raises: - FileNotFoundError: If path does not exist - NotADirectoryError: If path is not a directory - """ + """Get a tree representation of a directory.""" resolved = self._resolve_path(path) if not resolved.exists(): @@ -276,17 +217,6 @@ def _build_tree( include_hidden: bool, current_depth: int, ) -> TreeNode: - """Recursively build a tree structure. - - Args: - path: Current path - max_depth: Maximum depth - include_hidden: Include hidden files - current_depth: Current recursion depth - - Returns: - TreeNode for this directory - """ entry_type = EntryType.DIRECTORY if path.is_dir() else EntryType.FILE children: list[TreeNode] = [] @@ -296,48 +226,86 @@ def _build_tree( path.iterdir(), key=lambda x: (x.is_file(), x.name.lower()), ) - for entry in entries: - # Skip hidden files if not include_hidden and entry.name.startswith("."): continue - - # Skip common uninteresting directories if entry.is_dir() and entry.name in self.SKIP_DIRS: continue - - child = self._build_tree( - entry, - max_depth, - include_hidden, - current_depth + 1, - ) + child = self._build_tree(entry, max_depth, include_hidden, current_depth + 1) children.append(child) - except PermissionError: pass - return TreeNode( - name=path.name or str(path), - entry_type=entry_type, - children=children, - ) + return TreeNode(name=path.name or str(path), entry_type=entry_type, children=children) - def get_tree_string( + # ------------------------------------------------------------------ + # Write operations + # ------------------------------------------------------------------ + + def write_file( self, - path: str = "", - max_depth: int = 3, - include_hidden: bool = False, - ) -> str: - """Get a string representation of the directory tree. + path: str, + content: str, + content_type: str = "text/plain", + ) -> OperationResult: + """Write text content to a file. + + Creates parent directories as needed. Args: - path: Relative path to directory - max_depth: Maximum depth to traverse - include_hidden: Whether to include hidden files + path: Relative file path to write to. + content: Text content to write (UTF-8). + content_type: Ignored for local files; accepted for interface compatibility. Returns: - String representation of the tree + OperationResult indicating success or failure. """ - tree = self.get_tree(path, max_depth, include_hidden) - return tree.to_string() \ No newline at end of file + try: + resolved = self._resolve_path(path) + except ValueError as e: + return OperationResult(success=False, path=path, error=str(e)) + + if not path: + return OperationResult(success=False, path=path, error="Cannot write: path is empty") + + try: + resolved.parent.mkdir(parents=True, exist_ok=True) + resolved.write_text(content, encoding="utf-8") + return OperationResult( + success=True, + path=str(resolved.relative_to(self.root)), + message=f"Written {len(content.encode('utf-8'))} bytes to {resolved}", + ) + except Exception as e: + logger.error(f"write_file failed for {path!r}: {e}") + return OperationResult(success=False, path=path, error=str(e)) + + def delete_file(self, path: str) -> OperationResult: + """Delete a file. + + Args: + path: Relative file path to delete. + + Returns: + OperationResult indicating success or failure. + """ + try: + resolved = self._resolve_path(path) + except ValueError as e: + return OperationResult(success=False, path=path, error=str(e)) + + if not resolved.exists(): + return OperationResult(success=False, path=path, error=f"File not found: {path}") + if resolved.is_dir(): + return OperationResult(success=False, path=path, error=f"Path is a directory: {path}") + + try: + resolved.unlink() + return OperationResult( + success=True, + path=path, + message=f"Deleted {resolved}", + ) + except Exception as e: + logger.error(f"delete_file failed for {path!r}: {e}") + return OperationResult(success=False, path=path, error=str(e)) diff --git a/src/codespy/tools/filesystem/server.py b/src/codespy/tools/storage/filesystem/server.py similarity index 98% rename from src/codespy/tools/filesystem/server.py rename to src/codespy/tools/storage/filesystem/server.py index 2173351..3b6fcad 100644 --- a/src/codespy/tools/filesystem/server.py +++ b/src/codespy/tools/storage/filesystem/server.py @@ -8,7 +8,7 @@ from mcp.server.fastmcp import FastMCP -from codespy.tools.filesystem.client import FileSystem +from codespy.tools.storage.filesystem.client import FileSystem logger = logging.getLogger(__name__) @@ -148,7 +148,7 @@ def get_file_info(path: str = "") -> dict: # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + root = sys.argv[1] if len(sys.argv) > 1 else "." _fs = FileSystem(root) mcp.run() diff --git a/src/codespy/tools/filesystem/models.py b/src/codespy/tools/storage/models.py similarity index 59% rename from src/codespy/tools/filesystem/models.py rename to src/codespy/tools/storage/models.py index 1d81c07..1ca9d7b 100644 --- a/src/codespy/tools/filesystem/models.py +++ b/src/codespy/tools/storage/models.py @@ -1,4 +1,6 @@ -"""Data models for filesystem operations.""" +"""Shared data models for storage operations (filesystem and S3).""" + +from __future__ import annotations from datetime import datetime from enum import Enum @@ -8,14 +10,14 @@ class EntryType(str, Enum): - """Type of filesystem entry.""" + """Type of storage entry.""" FILE = "file" DIRECTORY = "directory" SYMLINK = "symlink" -class FileInfo(BaseModel): +class Info(BaseModel): """Information about a file or directory.""" path: str = Field(description="Relative path from root") @@ -24,17 +26,20 @@ class FileInfo(BaseModel): size: int = Field(default=0, description="Size in bytes (0 for directories)") modified_at: datetime | None = Field(default=None, description="Last modified time") extension: str = Field(default="", description="File extension (empty for directories)") + # S3-specific (empty strings for local filesystem) + etag: str = Field(default="", description="ETag (S3 only)") + storage_class: str = Field(default="", description="S3 storage class (S3 only)") @classmethod - def from_path(cls, path: Path, root: Path) -> "FileInfo": - """Create FileInfo from a Path object. + def from_path(cls, path: Path, root: Path) -> Info: + """Create Info from a local filesystem Path. Args: - path: The file path - root: Root directory to compute relative path + path: The file path. + root: Root directory to compute relative path. Returns: - FileInfo instance + Info instance. """ stat = path.stat() rel_path = str(path.relative_to(root)) @@ -56,19 +61,19 @@ def from_path(cls, path: Path, root: Path) -> "FileInfo": ) -class DirectoryEntry(BaseModel): +class Entry(BaseModel): """Entry in a directory listing.""" name: str = Field(description="Entry name") entry_type: EntryType = Field(description="Type of entry") - size: int = Field(default=0, description="Size in bytes") + size: int = Field(default=0, description="Size in bytes (0 for directories)") -class DirectoryListing(BaseModel): +class Listing(BaseModel): """Result of listing a directory.""" path: str = Field(description="Directory path") - entries: list[DirectoryEntry] = Field(default_factory=list, description="Directory contents") + entries: list[Entry] = Field(default_factory=list, description="Directory contents") total_files: int = Field(default=0, description="Number of files") total_directories: int = Field(default=0, description="Number of directories") @@ -84,11 +89,11 @@ def to_string(self, prefix: str = "", is_last: bool = True) -> str: """Convert tree node to string representation. Args: - prefix: Current line prefix - is_last: Whether this is the last sibling + prefix: Current line prefix. + is_last: Whether this is the last sibling. Returns: - String representation of the tree + String representation of the tree. """ connector = "└── " if is_last else "├── " icon = "📁 " if self.entry_type == EntryType.DIRECTORY else "📄 " @@ -101,11 +106,27 @@ def to_string(self, prefix: str = "", is_last: bool = True) -> str: return result -class FileContent(BaseModel): +class Content(BaseModel): """Result of reading a file.""" path: str = Field(description="File path") - content: str = Field(description="File content") - size: int = Field(description="Total file size in bytes") - lines: int = Field(description="Total number of lines") - truncated: bool = Field(default=False, description="Whether content was truncated") \ No newline at end of file + content: str = Field(default="", description="File content as text") + size: int = Field(default=0, description="Total file size in bytes") + lines: int = Field(default=0, description="Total number of lines") + truncated: bool = Field(default=False, description="Whether content was truncated") + content_type: str = Field(default="", description="MIME type (S3) or empty for local files") + error: str | None = Field(default=None, description="Error message if read failed") + + @property + def success(self) -> bool: + """Check if the file was read successfully.""" + return self.error is None + + +class OperationResult(BaseModel): + """Result of a write or delete operation.""" + + success: bool = Field(description="Whether the operation succeeded") + path: str = Field(description="File path that was operated on") + message: str = Field(default="", description="Human-readable result message") + error: str | None = Field(default=None, description="Error message if operation failed") diff --git a/src/codespy/tools/storage/s3/__init__.py b/src/codespy/tools/storage/s3/__init__.py new file mode 100644 index 0000000..73d2af2 --- /dev/null +++ b/src/codespy/tools/storage/s3/__init__.py @@ -0,0 +1,5 @@ +"""S3 storage backend.""" + +from codespy.tools.storage.s3.client import S3Client + +__all__ = ["S3Client"] diff --git a/src/codespy/tools/aws/s3/client.py b/src/codespy/tools/storage/s3/client.py similarity index 63% rename from src/codespy/tools/aws/s3/client.py rename to src/codespy/tools/storage/s3/client.py index 326e984..c6afcf5 100644 --- a/src/codespy/tools/aws/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -1,25 +1,28 @@ """S3 client for filesystem-like operations over a single bucket.""" +from __future__ import annotations + import logging -from codespy.tools.aws.s3.models import ( +from codespy.tools.storage.base import Storage +from codespy.tools.storage.models import ( + Content, + Entry, EntryType, + Info, + Listing, OperationResult, - S3Content, - S3Entry, - S3Info, - S3Listing, - S3TreeNode, + TreeNode, ) logger = logging.getLogger(__name__) -class S3Client: +class S3Client(Storage): """Client for S3 operations rooted at a single bucket. Treats S3 key prefixes as directories and individual object keys as files, - mirroring the FileSystem client interface. All path arguments are relative + mirroring the FileSystem client interface. All path arguments are relative to the bucket root (no leading slash needed). Authentication uses the standard boto3 credential chain: @@ -57,22 +60,7 @@ def __init__( # ------------------------------------------------------------------ def _resolve_path(self, path: str) -> str: - """Normalise a relative path, guarding against escaping the bucket root. - - Mirrors FileSystem._resolve_path(): strips leading slashes, collapses - redundant separators, rejects '..' traversal. - - Args: - path: User-supplied file path or directory prefix. - - Returns: - Normalised S3 key string (no leading slash). - - Raises: - ValueError: If path contains '..' components that escape the root. - """ normalised = path.lstrip("/") - parts = normalised.split("/") resolved: list[str] = [] for part in parts: @@ -80,36 +68,18 @@ def _resolve_path(self, path: str) -> str: raise ValueError(f"Path escapes bucket root: {path!r}") if part and part != ".": resolved.append(part) - return "/".join(resolved) def _file_name(self, path: str) -> str: - """Return the last component of a path (filename or directory name). - - Args: - path: S3 key or prefix. - - Returns: - Last path component. - """ return path.rstrip("/").rsplit("/", 1)[-1] def _file_extension(self, path: str) -> str: - """Return the file extension from a path (without leading dot). - - Args: - path: S3 object key. - - Returns: - Extension string, e.g. 'py', 'json', or '' if none. - """ name = self._file_name(path) if "." in name: return name.rsplit(".", 1)[-1] return "" def _client_error_code(self, exc: Exception) -> str: - """Safely extract the error code from a botocore ClientError.""" try: return exc.response["Error"]["Code"] # type: ignore[attr-defined] except Exception: @@ -120,24 +90,12 @@ def _client_error_code(self, exc: Exception) -> str: # ------------------------------------------------------------------ def exists(self, path: str = "") -> bool: - """Check whether a file or directory exists in the bucket. - - For files: uses HeadObject. - For directories: lists with the prefix — exists if any entries found. - - Args: - path: Relative file path or directory prefix to check. - - Returns: - True if the path exists. - """ + """Check whether a file or directory exists in the bucket.""" file_path = self._resolve_path(path) if not file_path: - # Bucket root always exists return True - # Try as an exact file first try: self._s3.head_object(Bucket=self.bucket, Key=file_path) return True @@ -145,7 +103,6 @@ def exists(self, path: str = "") -> bool: if self._client_error_code(e) not in ("404", "NoSuchKey"): logger.warning(f"HeadObject error for {file_path!r}: {e}") - # Try as a directory prefix dir_prefix = file_path if file_path.endswith("/") else file_path + "/" try: resp = self._s3.list_objects_v2( @@ -158,31 +115,20 @@ def exists(self, path: str = "") -> bool: logger.warning(f"ListObjectsV2 error for prefix {dir_prefix!r}: {e}") return False - def get_info(self, path: str = "") -> S3Info: - """Get metadata about a file or directory. - - Args: - path: Relative file path or directory prefix. - - Returns: - S3Info with metadata. - - Raises: - FileNotFoundError: If the path does not exist. - """ + def get_info(self, path: str = "") -> Info: + """Get metadata about a file or directory.""" file_path = self._resolve_path(path) if not file_path: - return S3Info( + return Info( path=".", name=self.bucket, entry_type=EntryType.DIRECTORY, ) - # Try as a file try: resp = self._s3.head_object(Bucket=self.bucket, Key=file_path) - return S3Info( + return Info( path=file_path, name=self._file_name(file_path), entry_type=EntryType.FILE, @@ -196,7 +142,6 @@ def get_info(self, path: str = "") -> S3Info: if self._client_error_code(e) not in ("404", "NoSuchKey"): raise - # Try as a directory prefix dir_prefix = file_path if file_path.endswith("/") else file_path + "/" resp = self._s3.list_objects_v2( Bucket=self.bucket, @@ -204,7 +149,7 @@ def get_info(self, path: str = "") -> S3Info: MaxKeys=1, ) if resp.get("Contents") or resp.get("CommonPrefixes"): - return S3Info( + return Info( path=file_path, name=self._file_name(file_path), entry_type=EntryType.DIRECTORY, @@ -216,22 +161,12 @@ def list_directory( self, path: str = "", include_hidden: bool = False, - ) -> S3Listing: - """List files and subdirectories directly under a path (one level deep). - - Uses S3 Delimiter="/" so sub-prefixes are returned as directories. - - Args: - path: Relative directory path to list (empty string = bucket root). - include_hidden: Whether to include entries starting with '.'. - - Returns: - S3Listing with entries sorted: directories first, then files. - """ + ) -> Listing: + """List files and subdirectories directly under a path (one level deep).""" dir_path = self._resolve_path(path) prefix = (dir_path + "/") if dir_path else "" - entries: list[S3Entry] = [] + entries: list[Entry] = [] total_files = 0 total_directories = 0 continuation_token: str | None = None @@ -252,29 +187,23 @@ def list_directory( logger.error(f"Error listing {prefix!r}: {e}") break - # CommonPrefixes → sub-directories for cp in resp.get("CommonPrefixes", []): sub = cp.get("Prefix", "") name = sub.rstrip("/").rsplit("/", 1)[-1] if not include_hidden and name.startswith("."): continue - entries.append(S3Entry(name=name, entry_type=EntryType.DIRECTORY)) + entries.append(Entry(name=name, entry_type=EntryType.DIRECTORY)) total_directories += 1 - # Contents → files (skip placeholder directory key) for obj in resp.get("Contents", []): file_key = obj.get("Key", "") if file_key == prefix: - continue # zero-byte folder placeholder + continue name = file_key.rsplit("/", 1)[-1] if not include_hidden and name.startswith("."): continue entries.append( - S3Entry( - name=name, - entry_type=EntryType.FILE, - size=obj.get("Size", 0), - ) + Entry(name=name, entry_type=EntryType.FILE, size=obj.get("Size", 0)) ) total_files += 1 @@ -282,10 +211,9 @@ def list_directory( break continuation_token = resp.get("NextContinuationToken") - # Directories first, then files — same sort as FileSystem.list_directory entries.sort(key=lambda e: (e.entry_type == EntryType.FILE, e.name.lower())) - return S3Listing( + return Listing( path=dir_path or ".", entries=entries, total_files=total_files, @@ -297,28 +225,16 @@ def read_file( path: str, max_bytes: int = 100_000, max_lines: int | None = None, - ) -> S3Content: - """Read a file from S3 as text. - - Mirrors FileSystem.read_file(): utf-8 → latin-1 fallback, byte and line - truncation, returns error in model rather than raising. - - Args: - path: Relative file path. - max_bytes: Maximum bytes to read (default 100 KB). - max_lines: Maximum lines to read (optional). - - Returns: - S3Content with file data. - """ + ) -> Content: + """Read a file from S3 as text.""" file_path = self._resolve_path(path) if not file_path: - return S3Content(path=path, error="Cannot read: path is empty (bucket root)") + return Content(path=path, error="Cannot read: path is empty (bucket root)") try: resp = self._s3.get_object(Bucket=self.bucket, Key=file_path) except Exception as e: - return S3Content(path=file_path, error=f"GetObject failed: {e}") + return Content(path=file_path, error=f"GetObject failed: {e}") size: int = resp.get("ContentLength", 0) content_type: str = resp.get("ContentType", "") @@ -327,25 +243,19 @@ def read_file( try: raw: bytes = resp["Body"].read(max_bytes + 1) except Exception as e: - return S3Content( - path=file_path, - error=f"Error reading body: {e}", - size=size, - content_type=content_type, - ) + return Content(path=file_path, error=f"Error reading body: {e}", size=size, content_type=content_type) if len(raw) > max_bytes: raw = raw[:max_bytes] truncated = True - # Decode: utf-8 first, latin-1 fallback (same as FileSystem.read_file) try: content = raw.decode("utf-8") except UnicodeDecodeError: try: content = raw.decode("latin-1") except Exception: - return S3Content( + return Content( path=file_path, error="Cannot decode file as text (binary content)", size=size, @@ -360,7 +270,7 @@ def read_file( content = "\n".join(lines[:max_lines]) truncated = True - return S3Content( + return Content( path=file_path, content=content, size=size, @@ -374,17 +284,8 @@ def get_tree( path: str = "", max_depth: int = 3, include_hidden: bool = False, - ) -> S3TreeNode: - """Get a tree representation of a directory in the bucket. - - Args: - path: Relative directory path to start from (empty = bucket root). - max_depth: Maximum recursion depth. - include_hidden: Whether to include entries starting with '.'. - - Returns: - S3TreeNode representing the directory tree. - """ + ) -> TreeNode: + """Get a tree representation of a directory in the bucket.""" dir_path = self._resolve_path(path) name = self._file_name(dir_path) if dir_path else self.bucket return self._build_tree(dir_path, name, max_depth, include_hidden, 0) @@ -396,63 +297,22 @@ def _build_tree( max_depth: int, include_hidden: bool, current_depth: int, - ) -> S3TreeNode: - """Recursively build an S3TreeNode. - - Args: - dir_path: Current directory path (S3 prefix). - name: Display name for this node. - max_depth: Maximum recursion depth. - include_hidden: Include hidden entries. - current_depth: Current recursion depth counter. - - Returns: - S3TreeNode for this directory. - """ + ) -> TreeNode: if current_depth >= max_depth: - return S3TreeNode(name=name, entry_type=EntryType.DIRECTORY) + return TreeNode(name=name, entry_type=EntryType.DIRECTORY) listing = self.list_directory(dir_path, include_hidden=include_hidden) - children: list[S3TreeNode] = [] + children: list[TreeNode] = [] for entry in listing.entries: if entry.entry_type == EntryType.DIRECTORY: child_path = f"{dir_path}/{entry.name}" if dir_path else entry.name - child = self._build_tree( - child_path, - entry.name, - max_depth, - include_hidden, - current_depth + 1, - ) + child = self._build_tree(child_path, entry.name, max_depth, include_hidden, current_depth + 1) else: - child = S3TreeNode(name=entry.name, entry_type=EntryType.FILE) + child = TreeNode(name=entry.name, entry_type=EntryType.FILE) children.append(child) - return S3TreeNode( - name=name, - entry_type=EntryType.DIRECTORY, - children=children, - ) - - def get_tree_string( - self, - path: str = "", - max_depth: int = 3, - include_hidden: bool = False, - ) -> str: - """Get a string representation of the directory tree. - - Args: - path: Relative directory path to start from. - max_depth: Maximum recursion depth. - include_hidden: Whether to include hidden entries. - - Returns: - String representation of the tree (same style as FileSystem.get_tree_string). - """ - tree = self.get_tree(path, max_depth, include_hidden) - return tree.to_string() + return TreeNode(name=name, entry_type=EntryType.DIRECTORY, children=children) # ------------------------------------------------------------------ # Write operations @@ -464,16 +324,7 @@ def write_file( content: str, content_type: str = "text/plain", ) -> OperationResult: - """Write text content to a file in the bucket. - - Args: - path: Relative file path to write to. - content: Text content to write (encoded as UTF-8). - content_type: MIME type for the object (default 'text/plain'). - - Returns: - OperationResult indicating success or failure. - """ + """Write text content to a file in the bucket.""" file_path = self._resolve_path(path) if not file_path: return OperationResult( @@ -501,14 +352,7 @@ def write_file( return OperationResult(success=False, path=file_path, error=str(e)) def delete_file(self, path: str) -> OperationResult: - """Delete a file from the bucket. - - Args: - path: Relative file path to delete. - - Returns: - OperationResult indicating success or failure. - """ + """Delete a file from the bucket.""" file_path = self._resolve_path(path) if not file_path: return OperationResult( diff --git a/src/codespy/tools/aws/s3/server.py b/src/codespy/tools/storage/s3/server.py similarity index 99% rename from src/codespy/tools/aws/s3/server.py rename to src/codespy/tools/storage/s3/server.py index 93a856d..2d43874 100644 --- a/src/codespy/tools/aws/s3/server.py +++ b/src/codespy/tools/storage/s3/server.py @@ -7,7 +7,7 @@ from mcp.server.fastmcp import FastMCP -from codespy.tools.aws.s3.client import S3Client +from codespy.tools.storage.s3.client import S3Client logger = logging.getLogger(__name__) From 9ab23a6afaa93cf6df6961d8a37e71a5f7f89de3 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 24 Jul 2026 18:19:38 +0200 Subject: [PATCH 08/79] add storage to hippocampus --- .../hippocampus/modules/cartographer.py | 18 ++++++++++ .../agents/hippocampus/modules/distiller.py | 34 +++++++++---------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/codespy/agents/hippocampus/modules/cartographer.py b/src/codespy/agents/hippocampus/modules/cartographer.py index d40cb2b..3064155 100644 --- a/src/codespy/agents/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/hippocampus/modules/cartographer.py @@ -41,6 +41,24 @@ class CartographerSig(dspy.Signature): completely DIFFERENT question about this context benefit from knowing this?" If not, it probably isn't worth the budget. + ## How to use item_tags + + The Distiller assigns each existing item a tag. Let it drive your ops: + - harmful / stale → DELETE the item (unless a corrected REPLACE is + clearly the better fix). + - helpful but verbose or redundant → REPLACE with a tighter version. + - helpful and already compact → leave it; don't spend an op. + - neutral → keep as-is; do not churn ops on neutral items. + + ## Operation rules + + Emit only well-formed operations that satisfy the schema: + - ADD: requires `section` (one of the five section names) and `content`. + - DELETE: requires `item_id`. + - REPLACE: requires `item_id` and `content`. + - Only reference `item_id`s that exist in the current map. Never invent + ids — new items get their ids assigned automatically on ADD. + ## Value Priority (highest to lowest) 1. context_understanding — entity/concept inventories (key actors, diff --git a/src/codespy/agents/hippocampus/modules/distiller.py b/src/codespy/agents/hippocampus/modules/distiller.py index 64adc07..42c5440 100644 --- a/src/codespy/agents/hippocampus/modules/distiller.py +++ b/src/codespy/agents/hippocampus/modules/distiller.py @@ -13,19 +13,13 @@ class DistillerSig(dspy.Signature): """You are an expert analyst reviewing an agent's execution trajectory after it interacted with a long external context to answer a question. - The context map prepended to the agent is a compact CACHE OF - UNDERSTANDING about the external context — not answers to specific - questions. It should accumulate structural knowledge that helps with - ANY future question on the same context, the way a human builds a - mental model after reading a document. - ## Key Principle: Cache Understanding, Not Answers - The context map captures the agent's evolving understanding of the - context — NOT answers to specific questions. Think of it as the mental - model a human builds after reading a document: structure, key entities, - relationships, and global summaries that help with ANY question about - the content. + The context map prepended to the agent is a compact CACHE OF + UNDERSTANDING about the external context — NOT answers to specific + questions. Think of it as the mental model a human builds after reading + a document: structure, key entities, relationships, and global summaries + that help with ANY future question on the same context. ## Orientation vs. Question-Specific Work @@ -43,7 +37,8 @@ class DistillerSig(dspy.Signature): ## Produce three outputs - 1. DIAGNOSIS — Brief analysis of: + 1. DIAGNOSIS — Brief (3-5 sentences; it feeds the next module's prompt, + so keep it terse) analysis of: - How many iterations the agent spent on orientation vs. question-specific work - Whether the agent re-discovered structural information that was @@ -101,6 +96,10 @@ class DistillerSig(dspy.Signature): Do NOT abstract away exact numeric values, enum sets, output field names/types — these are domain constants and must remain precise. + Assign each candidate to one of these exact section names (they map + onto the context map schema): context_understanding, domain_constants, + context_roadmap, reusable_results, parsing_schema. + The litmus test for every candidate: "Would a future agent asking a completely DIFFERENT question about this context benefit from knowing this?" @@ -111,16 +110,17 @@ class DistillerSig(dspy.Signature): question: str = dspy.InputField(desc="The question the agent was answering.") diagnosis: str = dspy.OutputField( - desc="Brief analysis of orientation vs. question-specific work, whether " - "structural info was re-discovered that should have been cached, and what " - "transferable understanding the agent built." + desc="Brief (3-5 sentence) analysis of orientation vs. question-specific work, " + "whether structural info was re-discovered that should have been cached, and " + "what transferable understanding the agent built. Feeds the Cartographer prompt." ) item_tags: dict[str, ItemTag] = dspy.OutputField( desc="Per-item-id tag for EVERY item currently in the context map. " "Keys must match existing item ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( - desc="Candidate items to add. Each <= ~80 tokens; structural/transferable only." + desc="Candidate items to add. Each <= ~80 tokens; structural/transferable only. " + "Each candidate's `section` must be one of the five section names above." ) @@ -137,5 +137,5 @@ def __init__(self): super().__init__() self.predict = dspy.Predict(DistillerSig) - def forward(self, trajectory: str, context_map: str, question: str): + def forward(self, trajectory: str, context_map: ContextMap, question: str): return self.predict(trajectory=trajectory, context_map=context_map, question=question) From ec588a807f3810018bf0195c527da3653ef4720f Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 24 Jul 2026 19:58:58 +0200 Subject: [PATCH 09/79] add storage to hippocampus --- src/codespy/agents/hippocampus/__init__.py | 4 +- src/codespy/agents/hippocampus/context_map.py | 32 ++++++- src/codespy/agents/hippocampus/episode.py | 89 ++++++++++++++++++ src/codespy/agents/hippocampus/hypocampus.py | 93 +++++++++++++------ src/codespy/agents/hippocampus/persistence.py | 50 ---------- tests/__init__.py | 0 6 files changed, 184 insertions(+), 84 deletions(-) create mode 100644 src/codespy/agents/hippocampus/episode.py delete mode 100644 src/codespy/agents/hippocampus/persistence.py create mode 100644 tests/__init__.py diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py index b2e1f13..088efe9 100644 --- a/src/codespy/agents/hippocampus/__init__.py +++ b/src/codespy/agents/hippocampus/__init__.py @@ -7,9 +7,10 @@ OpType, SectionName, ) +from codespy.agents.hippocampus.episode import Episode +from codespy.agents.hippocampus.hypocampus import Hypocampus from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig -from codespy.agents.hippocampus.hypocampus import Hypocampus __all__ = [ "CacheCandidate", @@ -18,6 +19,7 @@ "ContextMap", "Distiller", "DistillerSig", + "Episode", "Hypocampus", "Item", "ItemTag", diff --git a/src/codespy/agents/hippocampus/context_map.py b/src/codespy/agents/hippocampus/context_map.py index 443e331..c0cb8d1 100644 --- a/src/codespy/agents/hippocampus/context_map.py +++ b/src/codespy/agents/hippocampus/context_map.py @@ -77,15 +77,24 @@ class ContextMap(BaseModel): ) domain_constants: list[Item] = Field( default_factory=list, - description="Exact parameters, formulas, thresholds, reference values, enum sets, and output field requirements", + description=( + "Exact parameters, formulas, thresholds, reference values, " + "enum sets, and output field requirements" + ), ) parsing_schema: list[Item] = Field( default_factory=list, - description="How to parse and navigate the context's format: delimiters, boundary patterns, field structure", + description=( + "How to parse and navigate the context's format: " + "delimiters, boundary patterns, field structure" + ), ) reusable_results: list[Item] = Field( default_factory=list, - description="Agent-derived aggregated outputs (counts, distributions, classifications) that multiple questions would need", + description=( + "Agent-derived aggregated outputs (counts, distributions, classifications) " + "that multiple questions would need" + ), ) next_id: int = Field(default=1, exclude=True) @@ -171,3 +180,20 @@ def from_json(cls, text: str) -> ContextMap: max_n = max(max_n, int(suffix)) cm.next_id = max_n + 1 return cm + +def _recompute_next_id(cmap: ContextMap) -> None: + """Recompute ``cmap.next_id`` from the highest numeric item-ID suffix (in-place). + + After deserialisation the ``next_id`` counter is reset from the highest + numeric suffix found in any item ID (e.g. ``cu-00042`` → 42), so the + restored map can safely receive further ADD operations without collisions. + + Args: + cmap: The context map to update in-place. + """ + max_n = 0 + for item in cmap.all_items(): + suffix = item.id.rsplit("-", 1)[-1] + if suffix.isdigit(): + max_n = max(max_n, int(suffix)) + cmap.next_id = max_n + 1 diff --git a/src/codespy/agents/hippocampus/episode.py b/src/codespy/agents/hippocampus/episode.py new file mode 100644 index 0000000..14dd824 --- /dev/null +++ b/src/codespy/agents/hippocampus/episode.py @@ -0,0 +1,89 @@ +"""Episode record and persistence helpers for Hypocampus.""" + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, Field + +from codespy.agents.hippocampus.context_map import ContextMap, _recompute_next_id +from codespy.tools.storage.base import Storage + + +class Episode(BaseModel): + """A snapshot of an agent's consolidated memory at the end of an episode. + + Recorded by ``Hypocampus.end_episode()`` after the buffered trajectories + have been distilled into the context map. It captures *what the agent knew* + (the consolidated ``ContextMap``) together with lightweight identity and + timing metadata, so a review/run leaves behind a durable, inspectable + record of the memory it produced. + + Attributes: + task: Name of the wrapped agent's top-level signature (e.g. + ``"CodeReviewSignature"``). Falls back to the module class name when + the wrapped module exposes no signature. + module: Class name of the wrapped ``dspy.Module`` (e.g. ``"CodeReviewer"``). + context_map: Deep-copied snapshot of the context map *after* + consolidation, so later edits to the live map do not mutate this + record. + timestamp: UTC time the episode was recorded. + """ + + task: str = Field(description="Wrapped signature name (or module class name as fallback)") + module: str = Field(description="Wrapped dspy.Module class name") + context_map: ContextMap = Field(description="Consolidated context map snapshot") + timestamp: datetime = Field( + default_factory=datetime.utcnow, description="UTC time the episode was recorded" + ) + + +def save_episode(store: Storage, path: str, episode: Episode) -> None: + """Serialise ``episode`` to JSON and write it to ``path`` via ``store``. + + Args: + store: A ``FileSystem`` or ``S3Client`` instance. + path: Destination path (relative to the store's root / bucket). + episode: The episode to persist. + + Raises: + OSError: If the write operation fails. + """ + result = store.write_file( + path, episode.model_dump_json(indent=2), content_type="application/json" + ) + if not result.success: + raise OSError(f"Failed to save episode to {path!r}: {result.error}") + + +def load_episode(store: Storage, path: str) -> Episode: + """Load an episode from ``path`` via ``store``. + + The embedded ``ContextMap.next_id`` is recomputed from the loaded item IDs + so the restored map can safely receive further ADD operations. + + Args: + store: A ``FileSystem`` or ``S3Client`` instance. + path: Source path (relative to the store's root / bucket). + + Returns: + An ``Episode`` with ``context_map.next_id`` recomputed. + + Raises: + FileNotFoundError: If the path does not exist in the store. + OSError: If reading or parsing fails. + """ + result = store.read_file(path) + if not result.success: + error = result.error or "" + if "not found" in error.lower() or "NoSuchKey" in error: + raise FileNotFoundError(f"Episode not found at {path!r}: {error}") + raise OSError(f"Failed to load episode from {path!r}: {error}") + if not result.content: + raise OSError(f"Episode at {path!r} is empty") + try: + episode = Episode.model_validate_json(result.content) + except Exception as exc: + raise OSError(f"Failed to parse episode from {path!r}: {exc}") from exc + _recompute_next_id(episode.context_map) + return episode diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index fd3bb6f..8305135 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +from datetime import datetime import dspy @@ -12,10 +13,11 @@ format_trajectory, ) from codespy.agents.hippocampus.context_map import ContextMap, ItemTag +from codespy.agents.hippocampus.episode import Episode +from codespy.agents.hippocampus.episode import load_episode as _load_episode +from codespy.agents.hippocampus.episode import save_episode as _save_episode from codespy.agents.hippocampus.modules.cartographer import Cartographer from codespy.agents.hippocampus.modules.distiller import Distiller -from codespy.agents.hippocampus.persistence import load_map as _load_map -from codespy.agents.hippocampus.persistence import save_map as _save_map from codespy.tools.storage.base import Storage @@ -149,8 +151,17 @@ def __init__( self.question_field = question_field self.cmap = ContextMap() self.scores: dict[str, int] = {} - self._episode: list[str] = [] # per-call bounded trajectory strings - self._episode_question: str | None = None # derived from first buffered call + # Buffer of per-call bounded trajectory strings, cleared after end_episode(). + self._episode_trajectories: list[str] = [] + # Question derived from the first buffered call; used as Distiller input. + self._episode_question: str | None = None + # Identity of the wrapped module/signature for Episode metadata. + self._task_name: str = ( + top_sig.__name__ if top_sig is not None else type(module).__name__ + ) + self._module_name: str = type(module).__name__ + # The most recent consolidated Episode; set by end_episode(), None until then. + self.episode: Episode | None = None @property def current_map_text(self) -> str: @@ -160,12 +171,12 @@ def forward(self, **kwargs) -> dspy.Prediction: pred = self.agent(context_map=self.cmap, **kwargs) # Format once (stage-1 bounded); reused for both buffering and online reflect. traj = format_trajectory(pred, self.max_trajectory_tokens) - self._episode.append(traj) + self._episode_trajectories.append(traj) if self._episode_question is None: self._episode_question = self._make_question(kwargs) # Online reflection: None = no limit (always); N = for the first N calls. if (self.max_reflects is None - or len(self._episode) <= self.max_reflects): + or len(self._episode_trajectories) <= self.max_reflects): self._distill_and_apply(traj, self._make_question(kwargs)) return pred @@ -185,7 +196,7 @@ def end_episode( store: Storage | None = None, path: str | None = None, ) -> None: - """Consolidate the buffered episode into the map and clear the buffer. + """Consolidate the buffered trajectories into the map and record an Episode snapshot. A single Distiller pass sees all buffered trajectories joined with ``=== Call k ===`` headers. If ``max_trajectory_tokens`` is set, the @@ -193,49 +204,69 @@ def end_episode( (stage 1) already applied at append time. The question is derived from the first buffered call. No-op if the buffer is empty. - If both ``store`` and ``path`` are provided the updated map is - persisted after consolidation. ``store`` may be a ``FileSystem`` or - an ``S3Client`` instance. + After consolidation ``self.episode`` is set to a new :class:`Episode` + containing the task/module identity and a deep-copy snapshot of the + updated context map. + + If both ``store`` and ``path`` are provided the episode is persisted + via ``save_episode()`` after consolidation. ``store`` may be a + ``FileSystem`` or an ``S3Client`` instance. Args: - store: Optional ``Storage`` backend to persist the map after the - episode (``FileSystem`` or ``S3Client``). + store: Optional ``Storage`` backend to persist the episode after + consolidation (``FileSystem`` or ``S3Client``). path: Destination path within the store. Required when ``store`` is set. Raises: - IOError: If persistence is requested and the write fails. + OSError: If persistence is requested and the write fails. """ - if not self._episode: + if not self._episode_trajectories: return combined = "\n\n".join( - f"=== Call {i + 1} ===\n{t}" for i, t in enumerate(self._episode) + f"=== Call {i + 1} ===\n{t}" + for i, t in enumerate(self._episode_trajectories) ) if self.max_trajectory_tokens is not None: combined = _head_tail_text(combined, self.max_trajectory_tokens) self._distill_and_apply(combined, self._episode_question or "") - self._episode.clear() + # Record the consolidated episode as a snapshot of the updated map. + self.episode = Episode( + task=self._task_name, + module=self._module_name, + context_map=self.cmap.model_copy(deep=True), + timestamp=datetime.utcnow(), + ) + self._episode_trajectories.clear() self._episode_question = None if store is not None and path is not None: - _save_map(store, path, self.cmap) + _save_episode(store, path, self.episode) - def save_map(self, store: Storage, path: str) -> None: - """Persist the current context map to ``path`` via ``store``. + def save_episode(self, store: Storage, path: str) -> None: + """Persist the current episode to ``path`` via ``store``. Args: store: Storage backend (``FileSystem`` or ``S3Client``). path: Destination path within the store. Raises: - IOError: If the write fails. + ValueError: If no episode has been consolidated yet (call + ``end_episode()`` first). + OSError: If the write fails. """ - _save_map(store, path, self.cmap) + if self.episode is None: + raise ValueError( + "No episode to save — call end_episode() to consolidate first." + ) + _save_episode(store, path, self.episode) - def load_map(self, store: Storage, path: str) -> None: - """Replace the current map with one loaded from ``path`` via ``store``. + def load_episode(self, store: Storage, path: str) -> None: + """Replace the current state with an episode loaded from ``path`` via ``store``. - Resets ``scores`` and clears the episode buffer since they belong to - the previous (now discarded) map. + Restores both ``self.episode`` and the live context map + (``self.cmap = episode.context_map``) so the agent resumes from the + persisted state. Also resets ``scores`` and clears the trajectory + buffer since they belong to the previous state. Args: store: Storage backend (``FileSystem`` or ``S3Client``). @@ -243,16 +274,18 @@ def load_map(self, store: Storage, path: str) -> None: Raises: FileNotFoundError: If the path does not exist. - IOError: If reading or parsing fails. + OSError: If reading or parsing fails. """ - self.cmap = _load_map(store, path) + ep = _load_episode(store, path) + self.episode = ep + self.cmap = ep.context_map self.scores = {} - self._episode.clear() + self._episode_trajectories.clear() self._episode_question = None def reset_episode(self) -> None: - """Discard the buffered episode without reflecting.""" - self._episode.clear() + """Discard the buffered trajectories without reflecting.""" + self._episode_trajectories.clear() self._episode_question = None # ------------------------------------------------------------------ diff --git a/src/codespy/agents/hippocampus/persistence.py b/src/codespy/agents/hippocampus/persistence.py deleted file mode 100644 index 8b3e01a..0000000 --- a/src/codespy/agents/hippocampus/persistence.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Persistence helpers for ContextMap — filesystem and S3 backends.""" - -from __future__ import annotations - -from codespy.agents.hippocampus.context_map import ContextMap -from codespy.tools.storage.base import Storage - - -def save_map(store: Storage, path: str, cmap: ContextMap) -> None: - """Serialize ``cmap`` to JSON and write it to ``path`` via ``store``. - - Args: - store: A ``FileSystem`` or ``S3Client`` instance. - path: Destination path (relative to the store's root / bucket). - cmap: The context map to persist. - - Raises: - IOError: If the write operation fails. - """ - result = store.write_file(path, cmap.to_json(), content_type="application/json") - if not result.success: - raise IOError(f"Failed to save context map to {path!r}: {result.error}") - - -def load_map(store: Storage, path: str) -> ContextMap: - """Load a context map from ``path`` via ``store``. - - Args: - store: A ``FileSystem`` or ``S3Client`` instance. - path: Source path (relative to the store's root / bucket). - - Returns: - A ``ContextMap`` with ``next_id`` recomputed from loaded item IDs. - - Raises: - FileNotFoundError: If the path does not exist in the store. - IOError: If reading or parsing fails. - """ - result = store.read_file(path) - if not result.success: - error = result.error or "" - if "not found" in error.lower() or "NoSuchKey" in error: - raise FileNotFoundError(f"Context map not found at {path!r}: {error}") - raise IOError(f"Failed to load context map from {path!r}: {error}") - if not result.content: - raise IOError(f"Context map at {path!r} is empty") - try: - return ContextMap.from_json(result.content) - except Exception as exc: - raise IOError(f"Failed to parse context map from {path!r}: {exc}") from exc diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 8782c158bf4379e188ecafa2e7b8c4d4d87e891d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 00:00:59 +0200 Subject: [PATCH 10/79] wip --- codespy.yaml | 54 ++++++++ src/codespy/agents/hippocampus/hypocampus.py | 126 ++++++++++++++---- src/codespy/agents/reviewer/models.py | 12 ++ .../agents/reviewer/modules/code_reviewer.py | 25 +++- .../agents/reviewer/modules/doc_reviewer.py | 30 ++++- .../agents/reviewer/modules/helpers.py | 1 + .../reviewer/modules/scope_identifier.py | 70 +++++++--- .../reviewer/modules/supply_chain_auditor.py | 39 +++++- src/codespy/config.py | 51 ++++++- src/codespy/config_dspy.py | 80 ++++++++--- src/codespy/config_memory.py | 110 +++++++++++++++ 11 files changed, 521 insertions(+), 77 deletions(-) create mode 100644 src/codespy/config_memory.py diff --git a/codespy.yaml b/codespy.yaml index 516f0bb..9c24401 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -57,9 +57,38 @@ gitlab: url: https://gitlab.com # GITLAB_URL (for self-hosted instances) auto_discover_token: true # GITLAB_AUTO_DISCOVER_TOKEN (set to false to disable auto-discovery) +# ============================================================================ +# MEMORY +# ============================================================================ +# Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) +# consolidate their run into a ContextMap and persist it as an Episode. +# Save-only for now (no loading). Disabled by default per-signature — see +# `memory:` blocks under each signature below. +# +# Episodes are written to: +# episodes///codespy--.json +# under `root` (filesystem) or `s3_bucket` (s3). +memory: + backend: filesystem # MEMORY_BACKEND (filesystem | s3) + + # Filesystem backend + root: ~/.cache/codespy/memory # MEMORY_ROOT + + # S3 backend (used when backend: s3) + s3_bucket: null # MEMORY_S3_BUCKET + s3_region: null # MEMORY_S3_REGION (falls back to aws_region) + s3_endpoint_url: null # MEMORY_S3_ENDPOINT_URL (for MinIO/S3-compatible) + + # Reflection defaults — overridable per-signature via signatures..memory + default_enabled: false # MEMORY_DEFAULT_ENABLED + default_max_reflects: 0 # MEMORY_DEFAULT_MAX_REFLECTS (0 = reflect once at end_episode) + default_token_budget: 1024 # MEMORY_DEFAULT_TOKEN_BUDGET + default_max_trajectory_tokens: null # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS (null = full trajectory) + # ============================================================================ # SIGNATURES # ============================================================================ + # Each signature config supports: enabled, max_iters, model, max_context_size # Set to null to use defaults # @@ -112,6 +141,11 @@ signatures: scan_unchanged: false # SUPPLY_CHAIN_SCAN_UNCHANGED # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests # When false (default): only scans artifacts/manifests that were modified in the MR + memory: + enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS + token_budget: null # SUPPLY_CHAIN_MEMORY_TOKEN_BUDGET + max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS # Code Reviewer signature (bugs, security, removed defensive code, code smells) # Unified code review: bugs, security vulnerabilities, and code smells in a single agent pass per scope @@ -122,6 +156,11 @@ signatures: max_context_size: null # CODE_REVIEW_MAX_CONTEXT_SIZE max_reasoning_tokens: null # CODE_REVIEW_MAX_REASONING_TOKENS temperature: null # CODE_REVIEW_TEMPERATURE + memory: + enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS + token_budget: null # CODE_REVIEW_MEMORY_TOKEN_BUDGET + max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS # Documentation Reviewer signature (compares patches against extracted documentation) # Note: doc extraction is now deterministic (no LLM) — see doc_extractor.py @@ -131,6 +170,11 @@ signatures: max_context_size: null # DOC_MAX_CONTEXT_SIZE max_reasoning_tokens: null # DOC_MAX_REASONING_TOKENS temperature: null # DOC_TEMPERATURE + memory: + enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # DOC_MEMORY_MAX_REFLECTS + token_budget: null # DOC_MEMORY_TOKEN_BUDGET + max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS # Scope Identifier signature scope: @@ -140,6 +184,11 @@ signatures: max_context_size: null # SCOPE_MAX_CONTEXT_SIZE max_reasoning_tokens: null # SCOPE_MAX_REASONING_TOKENS temperature: null # SCOPE_TEMPERATURE + memory: + enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS + token_budget: null # SCOPE_MEMORY_TOKEN_BUDGET + max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS # Summarizer signature summarization: @@ -148,6 +197,11 @@ signatures: max_context_size: null # SUMMARIZATION_MAX_CONTEXT_SIZE max_reasoning_tokens: null # SUMMARIZATION_MAX_REASONING_TOKENS temperature: null # SUMMARIZATION_TEMPERATURE + memory: + enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) + max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS + token_budget: null # SUMMARIZATION_MEMORY_TOKEN_BUDGET + max_trajectory_tokens: null # SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS # ============================================================================ # OUTPUT diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 8305135..43779c4 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import copy from datetime import datetime @@ -75,6 +76,12 @@ class Hypocampus(dspy.Module): mem = Hypocampus(agent, max_reflects=0) pred = mem(task="…") # no end_episode() call + # Async variants (for callers running inside an event loop, e.g. + # reviewer modules using `await agent.acall(...)`) + mem = Hypocampus(agent, max_reflects=0) + pred = await mem.acall(task="…") + await mem.aend_episode(store, dir) + ## Trajectory bounding (two-stage) When ``max_trajectory_tokens`` is set: @@ -169,7 +176,28 @@ def current_map_text(self) -> str: def forward(self, **kwargs) -> dspy.Prediction: pred = self.agent(context_map=self.cmap, **kwargs) - # Format once (stage-1 bounded); reused for both buffering and online reflect. + self._buffer_and_maybe_reflect(pred, kwargs) + return pred + + async def aforward(self, **kwargs) -> dspy.Prediction: + """Async counterpart of :meth:`forward`. + + Awaits the wrapped agent's ``acall`` instead of invoking it + synchronously — required when the caller is already inside a running + event loop (e.g. reviewer modules using ``await agent.acall(...)``). + The Distiller/Cartographer reflection pass is still synchronous under + the hood but is offloaded to a thread so it never blocks the loop. + """ + pred = await self.agent.acall(context_map=self.cmap, **kwargs) + await asyncio.to_thread(self._buffer_and_maybe_reflect, pred, kwargs) + return pred + + def _buffer_and_maybe_reflect(self, pred: dspy.Prediction, kwargs: dict) -> None: + """Shared post-call work for both ``forward`` and ``aforward``. + + Buffers the (stage-1 bounded) trajectory and, depending on + ``max_reflects``, runs an online distill+apply pass immediately. + """ traj = format_trajectory(pred, self.max_trajectory_tokens) self._episode_trajectories.append(traj) if self._episode_question is None: @@ -178,7 +206,6 @@ def forward(self, **kwargs) -> dspy.Prediction: if (self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects): self._distill_and_apply(traj, self._make_question(kwargs)) - return pred def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: """Run one distillation cycle over ``pred``'s trajectory and update the map. @@ -191,10 +218,52 @@ def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: self._make_question(inputs), ) + def _consolidate(self) -> str | None: + """Join buffered trajectories (stage-2 bounded) and distill+apply once. + + Returns the combined trajectory text used for consolidation, or + ``None`` if the buffer is empty (no-op). + """ + if not self._episode_trajectories: + return None + combined = "\n\n".join( + f"=== Call {i + 1} ===\n{t}" + for i, t in enumerate(self._episode_trajectories) + ) + if self.max_trajectory_tokens is not None: + combined = _head_tail_text(combined, self.max_trajectory_tokens) + self._distill_and_apply(combined, self._episode_question or "") + return combined + + def _finalize_episode(self) -> None: + """Record the consolidated Episode snapshot and clear the buffer.""" + self.episode = Episode( + task=self._task_name, + module=self._module_name, + context_map=self.cmap.model_copy(deep=True), + timestamp=datetime.utcnow(), + ) + self._episode_trajectories.clear() + self._episode_question = None + + def _episode_file_path(self, dir: str) -> str: + """Build the full episode file path from a directory. + + Prepends the ``episodes`` root and appends a timestamped filename + named after the wrapped task: ``episodes//codespy--.json``. + + Args: + dir: Directory identifying where this episode belongs (e.g. a + scope's ``/{repo}/{subroot}/`` path). + """ + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + trimmed = dir.strip("/") + return f"episodes/{trimmed}/codespy-{self._task_name}-{timestamp}.json" + def end_episode( self, store: Storage | None = None, - path: str | None = None, + dir: str | None = None, ) -> None: """Consolidate the buffered trajectories into the map and record an Episode snapshot. @@ -208,39 +277,44 @@ def end_episode( containing the task/module identity and a deep-copy snapshot of the updated context map. - If both ``store`` and ``path`` are provided the episode is persisted - via ``save_episode()`` after consolidation. ``store`` may be a + If both ``store`` and ``dir`` are provided the episode is persisted + via ``save_episode()`` after consolidation, at + ``episodes//codespy--.json``. ``store`` may be a ``FileSystem`` or an ``S3Client`` instance. Args: store: Optional ``Storage`` backend to persist the episode after consolidation (``FileSystem`` or ``S3Client``). - path: Destination path within the store. Required when ``store`` - is set. + dir: Directory identifying where this episode belongs (e.g. a + scope's path). Required when ``store`` is set. Raises: OSError: If persistence is requested and the write fails. """ - if not self._episode_trajectories: + if self._consolidate() is None: return - combined = "\n\n".join( - f"=== Call {i + 1} ===\n{t}" - for i, t in enumerate(self._episode_trajectories) - ) - if self.max_trajectory_tokens is not None: - combined = _head_tail_text(combined, self.max_trajectory_tokens) - self._distill_and_apply(combined, self._episode_question or "") - # Record the consolidated episode as a snapshot of the updated map. - self.episode = Episode( - task=self._task_name, - module=self._module_name, - context_map=self.cmap.model_copy(deep=True), - timestamp=datetime.utcnow(), - ) - self._episode_trajectories.clear() - self._episode_question = None - if store is not None and path is not None: - _save_episode(store, path, self.episode) + self._finalize_episode() + if store is not None and dir is not None: + _save_episode(store, self._episode_file_path(dir), self.episode) + + async def aend_episode( + self, + store: Storage | None = None, + dir: str | None = None, + ) -> None: + """Async counterpart of :meth:`end_episode`. + + The (synchronous) Distiller/Cartographer consolidation pass and the + storage write are both offloaded to a thread so they never block the + caller's event loop. + """ + combined = await asyncio.to_thread(self._consolidate) + if combined is None: + return + await asyncio.to_thread(self._finalize_episode) + if store is not None and dir is not None: + path = self._episode_file_path(dir) + await asyncio.to_thread(_save_episode, store, path, self.episode) def save_episode(self, store: Storage, path: str) -> None: """Persist the current episode to ``path`` via ``store``. diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 0ab37ed..bd8a839 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -54,6 +54,9 @@ class PackageManifest(BaseModel): class ScopeResult(BaseModel): """A detected scope/subroot in the repository.""" + repo: str = Field( + default="", description="Repo identifier: 'owner/repo' (remote) or local dir name" + ) subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") has_changes: bool = Field( @@ -76,6 +79,15 @@ class ScopeResult(BaseModel): model_config = {"arbitrary_types_allowed": True} + def scope_path(self) -> str: + """Return the storage-relative path for this scope: ``/{repo}/{subroot}/``. + + Used by Hippocampus memory as the base directory for episode files. + ``subroot == "."`` (repo root) is normalized to ``"root"``. + """ + subroot = "root" if self.subroot in (".", "") else self.subroot.strip("/") + return f"/{self.repo}/{subroot}/" + class Issue(BaseModel): """Represents a single issue found during review.""" diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 237acbc..32d4088 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -16,6 +17,7 @@ restore_repo_paths, ) from codespy.config import get_settings +from codespy.config_memory import get_memory_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -229,10 +231,25 @@ async def aforward( f"({len(scope.changed_files)} files)" ) async with SignatureContext("code_review", self._cost_tracker): - result = await agent.acall( - scope=scoped, - categories=categories, - ) + if self._settings.get_memory_enabled("code_review"): + mem = Hypocampus( + agent, + token_budget=self._settings.get_memory_token_budget("code_review"), + max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( + "code_review" + ), + max_reflects=self._settings.get_memory_max_reflects("code_review"), + ) + result = await mem.aforward( + scope=scoped, + categories=categories, + ) + await mem.aend_episode(get_memory_store(self._settings), scope.scope_path()) + else: + result = await agent.acall( + scope=scoped, + categories=categories, + ) issues = [ issue for issue in (result.issues or []) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index b9530ff..cf13bce 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( @@ -17,6 +18,7 @@ restore_repo_paths, ) from codespy.config import get_settings +from codespy.config_memory import get_memory_store logger = logging.getLogger(__name__) @@ -161,12 +163,28 @@ async def aforward( f"({len(scope.changed_files)} files)" ) async with SignatureContext("doc", self._cost_tracker): - result = await asyncio.to_thread( - reviewer, - patches=patches, - documentation=documentation, - categories=[IssueCategory.DOCUMENTATION], - ) + if self._settings.get_memory_enabled("doc"): + mem = Hypocampus( + reviewer, + token_budget=self._settings.get_memory_token_budget("doc"), + max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( + "doc" + ), + max_reflects=self._settings.get_memory_max_reflects("doc"), + ) + result = await mem.aforward( + patches=patches, + documentation=documentation, + categories=[IssueCategory.DOCUMENTATION], + ) + await mem.aend_episode(get_memory_store(self._settings), scope.scope_path()) + else: + result = await asyncio.to_thread( + reviewer, + patches=patches, + documentation=documentation, + categories=[IssueCategory.DOCUMENTATION], + ) issues = [ issue for issue in (result.issues or []) if issue.confidence >= MIN_CONFIDENCE diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index 5e71b3a..1b57f99 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -151,6 +151,7 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: dependencies_changed=scope.package_manifest.dependencies_changed, ) return SR( + repo=scope.repo, subroot=".", scope_type=scope.scope_type, has_changes=scope.has_changes, diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index d3314c3..e2a0594 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -9,8 +9,10 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.hippocampus import Hypocampus from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType from codespy.config import get_settings +from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -216,10 +218,15 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal logger.warning("No reviewable files in MR - all files are binary, lock files, or in excluded directories") return [] + # Repo identifier for this review, stamped onto every ScopeResult + # (used by Hippocampus memory to build the episode path). + repo = repo_path.resolve().name if is_local else mr.repo_full_name + # Check if signature is enabled if not self._settings.is_signature_enabled("scope"): logger.warning("scope is disabled - using fallback single scope") return [ScopeResult( + repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, has_changes=True, @@ -230,6 +237,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal changed_files=reviewable_files, reason="Scope identification disabled - fallback to single scope", )] + tools, contexts = await self._create_mcp_tools(repo_path, is_local=is_local) changed_file_paths = [f.filename for f in reviewable_files] # Build map from filename to ChangedFile for post-processing @@ -249,25 +257,52 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal logger.info(f"Identifying scopes for {len(changed_file_paths)} changed files...") # Track scope signature costs async with SignatureContext("scope", self._cost_tracker): - result = await agent.acall( - changed_files=changed_file_paths, - repo_owner=mr.repo_owner, - repo_name=mr.repo_name, - head_sha=mr.head_sha, - target_repo_path=str(repo_path), - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", - is_local=is_local, - ) + if self._settings.get_memory_enabled("scope"): + mem = Hypocampus( + agent, + token_budget=self._settings.get_memory_token_budget("scope"), + max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( + "scope" + ), + max_reflects=self._settings.get_memory_max_reflects("scope"), + question_field="mr_title", + ) + result = await mem.aforward( + changed_files=changed_file_paths, + repo_owner=mr.repo_owner, + repo_name=mr.repo_name, + head_sha=mr.head_sha, + target_repo_path=str(repo_path), + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + is_local=is_local, + ) + # Repo-level episode: subroot "." (no scope object exists yet). + dir_path = f"/{repo}/root/" + await mem.aend_episode(get_memory_store(self._settings), dir_path) + else: + result = await agent.acall( + changed_files=changed_file_paths, + repo_owner=mr.repo_owner, + repo_name=mr.repo_name, + head_sha=mr.head_sha, + target_repo_path=str(repo_path), + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + is_local=is_local, + ) scope_assignments: list[ScopeAssignment] = result.scopes # Ensure we got valid scopes if not scope_assignments: raise ValueError("No scopes returned by agent") # Convert ScopeAssignment (with string paths) to ScopeResult (with ChangedFile objects) - scopes = self._convert_assignments_to_results(scope_assignments, changed_files_map) + scopes = self._convert_assignments_to_results( + scope_assignments, changed_files_map, repo + ) except Exception as e: logger.error(f"Agent failed: {e}") scopes = [ScopeResult( + repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, has_changes=True, @@ -286,16 +321,18 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal return scopes def _convert_assignments_to_results( - self, - assignments: list[ScopeAssignment], - changed_files_map: dict[str, ChangedFile] + self, + assignments: list[ScopeAssignment], + changed_files_map: dict[str, ChangedFile], + repo: str, ) -> list[ScopeResult]: """Convert LLM scope assignments to ScopeResults with proper ChangedFile objects. - + Args: assignments: Scope assignments from LLM with string file paths changed_files_map: Map from filename to ChangedFile object - + repo: Repo identifier stamped onto every ScopeResult (see ScopeResult.repo) + Returns: List of ScopeResult with ChangedFile objects instead of strings """ @@ -309,6 +346,7 @@ def _convert_assignments_to_results( else: logger.warning(f"File '{filepath}' from scope assignment not found in PR changed files") results.append(ScopeResult( + repo=repo, subroot=assignment.subroot, scope_type=assignment.scope_type, has_changes=assignment.has_changes, diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 3e5a56d..e2629ff 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,9 +8,11 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, resolve_scope_root, strip_prefix, restore_repo_paths from codespy.config import get_settings +from codespy.config_memory import get_memory_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -306,12 +308,37 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list ) # Track supply_chain signature costs separately async with SignatureContext("supply_chain", self._cost_tracker): - result = await supply_chain_agent.acall( - manifest_path=manifest_path, - lock_file_path=lock_file_path, - package_manager=package_manager, - category=IssueCategory.SECURITY, - ) + if self._settings.get_memory_enabled("supply_chain"): + mem = Hypocampus( + supply_chain_agent, + token_budget=self._settings.get_memory_token_budget( + "supply_chain" + ), + max_trajectory_tokens=( + self._settings.get_memory_max_trajectory_tokens( + "supply_chain" + ) + ), + max_reflects=self._settings.get_memory_max_reflects( + "supply_chain" + ), + ) + result = await mem.aforward( + manifest_path=manifest_path, + lock_file_path=lock_file_path, + package_manager=package_manager, + category=IssueCategory.SECURITY, + ) + await mem.aend_episode( + get_memory_store(self._settings), scope.scope_path() + ) + else: + result = await supply_chain_agent.acall( + manifest_path=manifest_path, + lock_file_path=lock_file_path, + package_manager=package_manager, + category=IssueCategory.SECURITY, + ) issues = [ issue for issue in result.issues if issue.confidence >= MIN_CONFIDENCE diff --git a/src/codespy/config.py b/src/codespy/config.py index 2756abd..15d78e8 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -27,6 +27,7 @@ discover_gemini_api_key, discover_openai_api_key, ) +from codespy.config_memory import MemoryConfig, reset_memory_store logger = logging.getLogger(__name__) @@ -44,6 +45,7 @@ "GitHubConfig", "GitLabConfig", "SignatureConfig", + "MemoryConfig", "OutputFormat", ] @@ -92,13 +94,14 @@ class Settings(BaseSettings): llm: LLMConfig = Field(default_factory=LLMConfig) github: GitHubConfig = Field(default_factory=GitHubConfig) gitlab: GitLabConfig = Field(default_factory=GitLabConfig) + memory: MemoryConfig = Field(default_factory=MemoryConfig) # Flat signature configs (signature_name -> SignatureConfig) signatures: dict[str, SignatureConfig] = Field(default_factory=dict) # Top-level defaults (also available via env vars DEFAULT_MODEL, etc.) default_model: str = "anthropic/claude-opus-4-6" - extraction_model: str | None = None # For TwoStepAdapter field extraction (falls back to default_model) + extraction_model: str | None = None # TwoStepAdapter extraction (falls back to default_model) default_max_iters: int = 3 default_max_context_size: int = 50000 default_max_reasoning_tokens: int = 8000 # Limit reasoning verbosity for adapter reliability @@ -190,6 +193,42 @@ def get_scan_unchanged(self, signature_name: str) -> bool: config = self.get_signature_config(signature_name) return config.scan_unchanged if config.scan_unchanged is not None else False + # Helper methods for per-signature memory config (Hippocampus) + def get_memory_enabled(self, signature_name: str) -> bool: + """Whether Hippocampus memory is enabled for a signature. + + Per-signature ``memory.enabled`` overrides ``memory.default_enabled``. + """ + config = self.get_signature_config(signature_name).memory + return config.enabled if config.enabled is not None else self.memory.default_enabled + + def get_memory_max_reflects(self, signature_name: str) -> int | None: + """Get max_reflects for a signature's memory (signature-specific or default).""" + config = self.get_signature_config(signature_name).memory + return ( + config.max_reflects + if config.max_reflects is not None + else self.memory.default_max_reflects + ) + + def get_memory_token_budget(self, signature_name: str) -> int: + """Get token_budget for a signature's memory (signature-specific or default).""" + config = self.get_signature_config(signature_name).memory + return ( + config.token_budget + if config.token_budget is not None + else self.memory.default_token_budget + ) + + def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: + """Get max_trajectory_tokens for a signature's memory (signature-specific or default).""" + config = self.get_signature_config(signature_name).memory + return ( + config.max_trajectory_tokens + if config.max_trajectory_tokens is not None + else self.memory.default_max_trajectory_tokens + ) + def log_signature_configs(self) -> None: """Log all signature configurations.""" logger.info("Signature configurations:") @@ -198,7 +237,11 @@ def log_signature_configs(self) -> None: model = sig_config.model or self.default_model max_iters = sig_config.max_iters or self.default_max_iters max_reasoning = sig_config.max_reasoning_tokens or self.default_max_reasoning_tokens - temp = sig_config.temperature if sig_config.temperature is not None else self.default_temperature + temp = ( + sig_config.temperature + if sig_config.temperature is not None + else self.default_temperature + ) logger.info( f" {sig_name}: {status}, model={model}, max_iters={max_iters}, " f"max_reasoning_tokens={max_reasoning}, temperature={temp}" @@ -450,6 +493,9 @@ def get_settings(config_file: str | None = None) -> Settings: def reload_settings(config_file: str | None = None) -> Settings: """Reload settings (useful after environment changes). + Also resets the cached Hippocampus memory store so a changed + ``memory`` configuration takes effect on next access. + Args: config_file: Optional path to a YAML config file. If provided, uses that file instead of the default locations. @@ -458,4 +504,5 @@ def reload_settings(config_file: str | None = None) -> Settings: if config_file is not None: _custom_config_path = config_file settings = Settings() + reset_memory_store() return settings diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index 32d847b..d7b7ffb 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -4,11 +4,24 @@ import os from typing import Any -from pydantic import BaseModel +from pydantic import BaseModel, Field logger = logging.getLogger(__name__) +class MemorySignatureConfig(BaseModel): + """Per-signature Hippocampus memory overrides. + + All fields are optional — ``None`` means "use the global memory default" + (see ``codespy.config_memory.MemoryConfig``). + """ + + enabled: bool | None = None # _MEMORY_ENABLED + max_reflects: int | None = None # _MEMORY_MAX_REFLECTS + token_budget: int | None = None # _MEMORY_TOKEN_BUDGET + max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS + + class SignatureConfig(BaseModel): """Configuration for a single signature.""" @@ -19,6 +32,7 @@ class SignatureConfig(BaseModel): max_reasoning_tokens: int | None = None # Limit reasoning verbosity for JSONAdapter reliability temperature: float | None = None # Lower = more deterministic JSON output scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests + memory: MemorySignatureConfig = Field(default_factory=MemorySignatureConfig) # Known signature names for env var routing @@ -34,7 +48,23 @@ class SignatureConfig(BaseModel): SIGNATURE_PREFIXES = {name.upper() + "_": name for name in SIGNATURE_NAMES} # Known signature settings for validation -SIGNATURE_SETTINGS = {"enabled", "max_iters", "model", "max_context_size", "max_reasoning_tokens", "temperature", "scan_unchanged"} +SIGNATURE_SETTINGS = { + "enabled", + "max_iters", + "model", + "max_context_size", + "max_reasoning_tokens", + "temperature", + "scan_unchanged", +} + +# Known per-signature memory settings, routed via _MEMORY_ +MEMORY_SIGNATURE_SETTINGS = { + "enabled", + "max_reflects", + "token_budget", + "max_trajectory_tokens", +} def convert_env_value(value: str) -> Any: @@ -58,12 +88,14 @@ def convert_env_value(value: str) -> Any: def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: """Apply environment variable overrides to config for signature settings. - Handles signature settings with pattern: - - CODE_REVIEW_MAX_ITERS -> signatures.code_review.max_iters - - SUPPLY_CHAIN_ENABLED -> signatures.supply_chain.enabled + Handles three patterns: + - ``CODE_REVIEW_MAX_ITERS`` -> signatures.code_review.max_iters + - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled + - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled + - ``SCOPE_MEMORY_TOKEN_BUDGET`` -> signatures.scope.memory.token_budget - Top-level settings (DEFAULT_MODEL, AWS_REGION, etc.) are handled directly - by pydantic-settings and should NOT be processed here. + Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) + are handled directly by pydantic-settings and should NOT be processed here. """ # Load .env file first to ensure env vars are available from dotenv import dotenv_values @@ -75,27 +107,41 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: continue key_upper = key.upper() - # Only process signature-specific settings + # Match signature prefix signature_name = None - setting = None - + remainder = None for prefix, sig_name in SIGNATURE_PREFIXES.items(): if key_upper.startswith(prefix): signature_name = sig_name - setting = key_upper[len(prefix) :].lower() + remainder = key_upper[len(prefix):] break - # Skip if not a signature setting or not a valid setting name - if not signature_name or setting not in SIGNATURE_SETTINGS: + if not signature_name or remainder is None: + continue + + # Nested memory setting: _MEMORY_ + if remainder.startswith("MEMORY_"): + memory_setting = remainder[len("MEMORY_"):].lower() + if memory_setting not in MEMORY_SIGNATURE_SETTINGS: + continue + if "signatures" not in config: + config["signatures"] = {} + if signature_name not in config["signatures"]: + config["signatures"][signature_name] = {} + if "memory" not in config["signatures"][signature_name]: + config["signatures"][signature_name]["memory"] = {} + sig_memory = config["signatures"][signature_name]["memory"] + sig_memory[memory_setting] = convert_env_value(value) continue - # Ensure signatures dict exists + # Flat signature setting + setting = remainder.lower() + if setting not in SIGNATURE_SETTINGS: + continue if "signatures" not in config: config["signatures"] = {} if signature_name not in config["signatures"]: config["signatures"][signature_name] = {} - - # Set the value config["signatures"][signature_name][setting] = convert_env_value(value) - return config \ No newline at end of file + return config diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py new file mode 100644 index 0000000..9d2341a --- /dev/null +++ b/src/codespy/config_memory.py @@ -0,0 +1,110 @@ +"""Memory (Hippocampus) configuration and storage factory.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +from pydantic import BaseModel, Field + +from codespy.tools.storage.base import Storage + +if TYPE_CHECKING: + from codespy.config import Settings + + +MemoryBackend = Literal["filesystem", "s3"] + + +class MemoryConfig(BaseModel): + """Global memory (Hippocampus) configuration. + + Controls where episodes are persisted and the default reflection knobs + applied to every agent. Per-signature ``memory:`` blocks override the + ``default_*`` values. + """ + + # Storage backend + backend: MemoryBackend = "filesystem" # MEMORY_BACKEND + root: str = "~/.cache/codespy/memory" # MEMORY_ROOT (filesystem backend) + s3_bucket: str | None = None # MEMORY_S3_BUCKET (s3 backend) + s3_region: str | None = None # MEMORY_S3_REGION (falls back to aws_region) + s3_endpoint_url: str | None = None # MEMORY_S3_ENDPOINT_URL (MinIO/S3-compatible) + + # Reflection defaults — overridable per-signature + default_enabled: bool = False # MEMORY_DEFAULT_ENABLED + default_max_reflects: int = Field(default=0) # MEMORY_DEFAULT_MAX_REFLECTS + default_token_budget: int = Field(default=1024) # MEMORY_DEFAULT_TOKEN_BUDGET + default_max_trajectory_tokens: int | None = None # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS + + +# Cached singleton store. Avoids reconstructing an S3Client's boto3 client +# (credential resolution + connection pool setup) on every call — see +# get_memory_store() for details. Filesystem stores are cheap to build but +# there's no reason not to reuse them too. +_store: Storage | None = None +_store_built = False + + +def get_memory_store(settings: Settings) -> Storage | None: + """Return the cached Storage backend for Hippocampus memory, or None if disabled. + + The store is built once and cached (module-level singleton). This matters + most for the S3 backend: constructing ``S3Client`` creates a boto3 client, + which resolves credentials and sets up a connection pool — work we don't + want repeated on every scope/signature call. Filesystem stores are cheap + to build, but caching them too keeps the function's behaviour uniform. + + Call :func:`reset_memory_store` after changing settings (e.g. via + ``reload_settings``) to force a rebuild on next access. + + Filesystem backend: creates a ``FileSystem`` rooted at the resolved + ``memory.root`` path (``~`` is expanded). + + S3 backend: creates an ``S3Client`` pointing at ``memory.s3_bucket`` with + optional region / endpoint overrides. Returns None if no bucket is configured. + + Args: + settings: Application settings. + + Returns: + Cached Storage instance, or None if storage is not configured. + """ + global _store, _store_built + if _store_built: + return _store + + mem = settings.memory + + if mem.backend == "s3": + if not mem.s3_bucket: + _store = None + else: + from codespy.tools.storage.s3.client import S3Client + + _store = S3Client( + bucket=mem.s3_bucket, + region=mem.s3_region or settings.aws_region, + endpoint_url=mem.s3_endpoint_url or None, + ) + else: + # Filesystem (default) + from pathlib import Path + + from codespy.tools.storage.filesystem.client import FileSystem + + root = str(Path(mem.root).expanduser().resolve()) + _store = FileSystem(root) + + _store_built = True + return _store + + +def reset_memory_store() -> None: + """Clear the cached memory store so it is rebuilt on next access. + + Call this after reloading settings (e.g. ``reload_settings()``) so a + changed ``memory`` configuration takes effect. + """ + global _store, _store_built + _store = None + _store_built = False From 1e2dc8ceb2ce1cf1b30dd05222008096ccc1a777 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 11:56:24 +0200 Subject: [PATCH 11/79] wip --- src/codespy/agents/hippocampus/hypocampus.py | 42 ++++++++++---------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/hippocampus/hypocampus.py index 43779c4..056753c 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/hippocampus/hypocampus.py @@ -160,8 +160,14 @@ def __init__( self.scores: dict[str, int] = {} # Buffer of per-call bounded trajectory strings, cleared after end_episode(). self._episode_trajectories: list[str] = [] + # Count of buffered trajectories already distilled online (via max_reflects). + # Used to skip a redundant consolidation distill in the single-call case, + # and to detect "everything was reflected online" so end_episode() still + # persists a snapshot even when nothing remains to consolidate. + self._reflected_count: int = 0 # Question derived from the first buffered call; used as Distiller input. self._episode_question: str | None = None + # Identity of the wrapped module/signature for Episode metadata. self._task_name: str = ( top_sig.__name__ if top_sig is not None else type(module).__name__ @@ -176,7 +182,7 @@ def current_map_text(self) -> str: def forward(self, **kwargs) -> dspy.Prediction: pred = self.agent(context_map=self.cmap, **kwargs) - self._buffer_and_maybe_reflect(pred, kwargs) + self._buffer_and_distill(pred, kwargs) return pred async def aforward(self, **kwargs) -> dspy.Prediction: @@ -189,10 +195,10 @@ async def aforward(self, **kwargs) -> dspy.Prediction: the hood but is offloaded to a thread so it never blocks the loop. """ pred = await self.agent.acall(context_map=self.cmap, **kwargs) - await asyncio.to_thread(self._buffer_and_maybe_reflect, pred, kwargs) + await asyncio.to_thread(self._buffer_and_distill, pred, kwargs) return pred - def _buffer_and_maybe_reflect(self, pred: dspy.Prediction, kwargs: dict) -> None: + def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None: """Shared post-call work for both ``forward`` and ``aforward``. Buffers the (stage-1 bounded) trajectory and, depending on @@ -205,18 +211,8 @@ def _buffer_and_maybe_reflect(self, pred: dspy.Prediction, kwargs: dict) -> None # Online reflection: None = no limit (always); N = for the first N calls. if (self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects): - self._distill_and_apply(traj, self._make_question(kwargs)) - - def reflect(self, pred: dspy.Prediction, inputs: dict) -> None: - """Run one distillation cycle over ``pred``'s trajectory and update the map. - - For manual / selective reflection outside of the automatic per-call flow. - The trajectory is bounded by ``max_trajectory_tokens`` (stage 1). - """ - self._distill_and_apply( - format_trajectory(pred, self.max_trajectory_tokens), - self._make_question(inputs), - ) + self._distill(traj, self._make_question(kwargs)) + self._reflected_count += 1 def _consolidate(self) -> str | None: """Join buffered trajectories (stage-2 bounded) and distill+apply once. @@ -224,7 +220,8 @@ def _consolidate(self) -> str | None: Returns the combined trajectory text used for consolidation, or ``None`` if the buffer is empty (no-op). """ - if not self._episode_trajectories: + skip_double_distill = len(self._episode_trajectories)==1 and self._reflected_count>0 + if not self._episode_trajectories or skip_double_distill: return None combined = "\n\n".join( f"=== Call {i + 1} ===\n{t}" @@ -232,7 +229,7 @@ def _consolidate(self) -> str | None: ) if self.max_trajectory_tokens is not None: combined = _head_tail_text(combined, self.max_trajectory_tokens) - self._distill_and_apply(combined, self._episode_question or "") + self._distill(combined, self._episode_question or "") return combined def _finalize_episode(self) -> None: @@ -245,6 +242,7 @@ def _finalize_episode(self) -> None: ) self._episode_trajectories.clear() self._episode_question = None + self._reflected_count = 0 def _episode_file_path(self, dir: str) -> str: """Build the full episode file path from a directory. @@ -291,7 +289,8 @@ def end_episode( Raises: OSError: If persistence is requested and the write fails. """ - if self._consolidate() is None: + nothing_to_persist = self._consolidate() is None and self._reflected_count==0 + if nothing_to_persist: return self._finalize_episode() if store is not None and dir is not None: @@ -309,7 +308,8 @@ async def aend_episode( caller's event loop. """ combined = await asyncio.to_thread(self._consolidate) - if combined is None: + nothing_to_persist = combined is None and self._reflected_count == 0 + if nothing_to_persist: return await asyncio.to_thread(self._finalize_episode) if store is not None and dir is not None: @@ -356,11 +356,13 @@ def load_episode(self, store: Storage, path: str) -> None: self.scores = {} self._episode_trajectories.clear() self._episode_question = None + self._reflected_count = 0 def reset_episode(self) -> None: """Discard the buffered trajectories without reflecting.""" self._episode_trajectories.clear() self._episode_question = None + self._reflected_count = 0 # ------------------------------------------------------------------ # Internals @@ -371,7 +373,7 @@ def _make_question(self, inputs: dict) -> str: return str(inputs.get(self.question_field, "")) return format_inputs(inputs, self.max_input_tokens) - def _distill_and_apply(self, trajectory: str, question: str) -> None: + def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, context_map=self.cmap, From df303820868b93ad8d8b91b530ee54d9df6594ce Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 11:59:21 +0200 Subject: [PATCH 12/79] wip --- src/codespy/agents/hippocampus/__init__.py | 29 ------------------- .../agents/hippocampus/modules/__init__.py | 11 ------- src/codespy/agents/memory/__init__.py | 0 .../agents/memory/hippocampus/__init__.py | 29 +++++++++++++++++++ .../agents/{ => memory}/hippocampus/budget.py | 2 +- .../{ => memory}/hippocampus/context_map.py | 0 .../{ => memory}/hippocampus/episode.py | 2 +- .../{ => memory}/hippocampus/hypocampus.py | 14 ++++----- .../memory/hippocampus/modules/__init__.py | 11 +++++++ .../hippocampus/modules/cartographer.py | 2 +- .../hippocampus/modules/distiller.py | 2 +- .../agents/reviewer/modules/code_reviewer.py | 2 +- .../agents/reviewer/modules/doc_reviewer.py | 2 +- .../reviewer/modules/scope_identifier.py | 2 +- .../reviewer/modules/supply_chain_auditor.py | 2 +- 15 files changed, 55 insertions(+), 55 deletions(-) delete mode 100644 src/codespy/agents/hippocampus/__init__.py delete mode 100644 src/codespy/agents/hippocampus/modules/__init__.py create mode 100644 src/codespy/agents/memory/__init__.py create mode 100644 src/codespy/agents/memory/hippocampus/__init__.py rename src/codespy/agents/{ => memory}/hippocampus/budget.py (99%) rename src/codespy/agents/{ => memory}/hippocampus/context_map.py (100%) rename src/codespy/agents/{ => memory}/hippocampus/episode.py (97%) rename src/codespy/agents/{ => memory}/hippocampus/hypocampus.py (97%) create mode 100644 src/codespy/agents/memory/hippocampus/modules/__init__.py rename src/codespy/agents/{ => memory}/hippocampus/modules/cartographer.py (99%) rename src/codespy/agents/{ => memory}/hippocampus/modules/distiller.py (99%) diff --git a/src/codespy/agents/hippocampus/__init__.py b/src/codespy/agents/hippocampus/__init__.py deleted file mode 100644 index 088efe9..0000000 --- a/src/codespy/agents/hippocampus/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from codespy.agents.hippocampus.context_map import ( - CacheCandidate, - ContextMap, - Item, - ItemTag, - Operation, - OpType, - SectionName, -) -from codespy.agents.hippocampus.episode import Episode -from codespy.agents.hippocampus.hypocampus import Hypocampus -from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig -from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig - -__all__ = [ - "CacheCandidate", - "Cartographer", - "CartographerSig", - "ContextMap", - "Distiller", - "DistillerSig", - "Episode", - "Hypocampus", - "Item", - "ItemTag", - "Operation", - "OpType", - "SectionName", -] diff --git a/src/codespy/agents/hippocampus/modules/__init__.py b/src/codespy/agents/hippocampus/modules/__init__.py deleted file mode 100644 index 2c325bf..0000000 --- a/src/codespy/agents/hippocampus/modules/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""DSPy modules for the hippocampus agent.""" - -from codespy.agents.hippocampus.modules.cartographer import Cartographer, CartographerSig -from codespy.agents.hippocampus.modules.distiller import Distiller, DistillerSig - -__all__ = [ - "Cartographer", - "CartographerSig", - "Distiller", - "DistillerSig", -] diff --git a/src/codespy/agents/memory/__init__.py b/src/codespy/agents/memory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py new file mode 100644 index 0000000..88f955d --- /dev/null +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -0,0 +1,29 @@ +from codespy.agents.memory.hippocampus.context_map import ( + CacheCandidate, + ContextMap, + Item, + ItemTag, + Operation, + OpType, + SectionName, +) +from codespy.agents.memory.hippocampus.episode import Episode +from codespy.agents.memory.hippocampus.hypocampus import Hypocampus +from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig +from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig + +__all__ = [ + "CacheCandidate", + "Cartographer", + "CartographerSig", + "ContextMap", + "Distiller", + "DistillerSig", + "Episode", + "Hypocampus", + "Item", + "ItemTag", + "Operation", + "OpType", + "SectionName", +] diff --git a/src/codespy/agents/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py similarity index 99% rename from src/codespy/agents/hippocampus/budget.py rename to src/codespy/agents/memory/hippocampus/budget.py index 2a8565c..004a62c 100644 --- a/src/codespy/agents/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -3,7 +3,7 @@ import dspy import tiktoken -from codespy.agents.hippocampus.context_map import ContextMap +from codespy.agents.memory.hippocampus.context_map import ContextMap _ENCODING = tiktoken.get_encoding("o200k_base") diff --git a/src/codespy/agents/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py similarity index 100% rename from src/codespy/agents/hippocampus/context_map.py rename to src/codespy/agents/memory/hippocampus/context_map.py diff --git a/src/codespy/agents/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py similarity index 97% rename from src/codespy/agents/hippocampus/episode.py rename to src/codespy/agents/memory/hippocampus/episode.py index 14dd824..f154cfa 100644 --- a/src/codespy/agents/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field -from codespy.agents.hippocampus.context_map import ContextMap, _recompute_next_id +from codespy.agents.memory.hippocampus.context_map import ContextMap, _recompute_next_id from codespy.tools.storage.base import Storage diff --git a/src/codespy/agents/hippocampus/hypocampus.py b/src/codespy/agents/memory/hippocampus/hypocampus.py similarity index 97% rename from src/codespy/agents/hippocampus/hypocampus.py rename to src/codespy/agents/memory/hippocampus/hypocampus.py index 056753c..3bfdc2c 100644 --- a/src/codespy/agents/hippocampus/hypocampus.py +++ b/src/codespy/agents/memory/hippocampus/hypocampus.py @@ -6,19 +6,19 @@ import dspy -from codespy.agents.hippocampus.budget import ( +from codespy.agents.memory.hippocampus.budget import ( _head_tail_text, count_tokens, evict, format_inputs, format_trajectory, ) -from codespy.agents.hippocampus.context_map import ContextMap, ItemTag -from codespy.agents.hippocampus.episode import Episode -from codespy.agents.hippocampus.episode import load_episode as _load_episode -from codespy.agents.hippocampus.episode import save_episode as _save_episode -from codespy.agents.hippocampus.modules.cartographer import Cartographer -from codespy.agents.hippocampus.modules.distiller import Distiller +from codespy.agents.memory.hippocampus.context_map import ContextMap, ItemTag +from codespy.agents.memory.hippocampus.episode import Episode +from codespy.agents.memory.hippocampus.episode import load_episode as _load_episode +from codespy.agents.memory.hippocampus.episode import save_episode as _save_episode +from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer +from codespy.agents.memory.hippocampus.modules.distiller import Distiller from codespy.tools.storage.base import Storage diff --git a/src/codespy/agents/memory/hippocampus/modules/__init__.py b/src/codespy/agents/memory/hippocampus/modules/__init__.py new file mode 100644 index 0000000..ac608f7 --- /dev/null +++ b/src/codespy/agents/memory/hippocampus/modules/__init__.py @@ -0,0 +1,11 @@ +"""DSPy modules for the hippocampus agent.""" + +from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig +from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig + +__all__ = [ + "Cartographer", + "CartographerSig", + "Distiller", + "DistillerSig", +] diff --git a/src/codespy/agents/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py similarity index 99% rename from src/codespy/agents/hippocampus/modules/cartographer.py rename to src/codespy/agents/memory/hippocampus/modules/cartographer.py index 3064155..8ab0a0c 100644 --- a/src/codespy/agents/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -2,7 +2,7 @@ import dspy -from codespy.agents.hippocampus.context_map import ( +from codespy.agents.memory.hippocampus.context_map import ( CacheCandidate, ContextMap, ItemTag, diff --git a/src/codespy/agents/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py similarity index 99% rename from src/codespy/agents/hippocampus/modules/distiller.py rename to src/codespy/agents/memory/hippocampus/modules/distiller.py index 42c5440..640d1b7 100644 --- a/src/codespy/agents/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -2,7 +2,7 @@ import dspy -from codespy.agents.hippocampus.context_map import ( +from codespy.agents.memory.hippocampus.context_map import ( CacheCandidate, ContextMap, ItemTag, diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 32d4088..7b32dcc 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index cf13bce..c990f9a 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index e2a0594..552942a 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hypocampus from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType from codespy.config import get_settings from codespy.config_memory import get_memory_store diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index e2629ff..e4bfe3a 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hypocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, resolve_scope_root, strip_prefix, restore_repo_paths from codespy.config import get_settings From f30ae81c446a23238a455be0eb420df06736d8d6 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 14:04:11 +0200 Subject: [PATCH 13/79] wip --- .../reviewer/modules/scope_identifier.py | 6 +++-- src/codespy/tools/git/github_client.py | 1 + src/codespy/tools/git/gitlab_client.py | 9 +++++++ src/codespy/tools/git/local_diff.py | 26 ++++++++++++------- src/codespy/tools/git/models.py | 21 ++++++++++++++- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 552942a..81506e0 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -219,8 +219,10 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal return [] # Repo identifier for this review, stamped onto every ScopeResult - # (used by Hippocampus memory to build the episode path). - repo = repo_path.resolve().name if is_local else mr.repo_full_name + # (used by Hippocampus memory to build the episode path). Uses the + # host-qualified slug (e.g. "github.com/owner/repo") so local and + # remote reviews of the same repository share the same memory path. + repo = mr.repo_slug # Check if signature is enabled if not self._settings.is_signature_enabled("scope"): diff --git a/src/codespy/tools/git/github_client.py b/src/codespy/tools/git/github_client.py index 5452319..61c194d 100644 --- a/src/codespy/tools/git/github_client.py +++ b/src/codespy/tools/git/github_client.py @@ -116,6 +116,7 @@ def fetch_merge_request(self, url: str) -> MergeRequest: updated_at=gh_pr.updated_at, repo_owner=owner, repo_name=repo_name, + host="github.com", changed_files=changed_files, labels=[label.name for label in gh_pr.labels], platform=GitPlatform.GITHUB, diff --git a/src/codespy/tools/git/gitlab_client.py b/src/codespy/tools/git/gitlab_client.py index 38e782a..64c4213 100644 --- a/src/codespy/tools/git/gitlab_client.py +++ b/src/codespy/tools/git/gitlab_client.py @@ -99,6 +99,13 @@ def _get_project_path(self, url: str) -> str: raise ValueError(f"Invalid GitLab MR URL: {url}") return match.group("path") + def _get_host(self, url: str) -> str: + """Get the host (e.g. 'gitlab.com' or a self-hosted domain) from URL.""" + match = self.MR_URL_PATTERN.match(url) + if not match: + raise ValueError(f"Invalid GitLab MR URL: {url}") + return match.group("host") + def _map_status(self, diff_status: str) -> FileStatus: """Map GitLab diff status to FileStatus enum.""" status_map = { @@ -119,6 +126,7 @@ def fetch_merge_request(self, url: str) -> MergeRequest: """ namespace, project_name, mr_number = self.parse_url(url) project_path = self._get_project_path(url) + host = self._get_host(url) # Get project and MR project = self.gitlab_client.projects.get(project_path) @@ -174,6 +182,7 @@ def fetch_merge_request(self, url: str) -> MergeRequest: updated_at=gl_mr.updated_at, repo_owner=namespace, repo_name=project_name, + host=host, changed_files=changed_files, labels=gl_mr.labels, platform=GitPlatform.GITLAB, diff --git a/src/codespy/tools/git/local_diff.py b/src/codespy/tools/git/local_diff.py index cc5856a..2eae042 100644 --- a/src/codespy/tools/git/local_diff.py +++ b/src/codespy/tools/git/local_diff.py @@ -49,33 +49,39 @@ def _count_diff_lines(patch: str) -> tuple[int, int]: return additions, deletions -def _get_repo_info(repo_path: Path) -> tuple[str, str]: - """Extract owner and repo name from git remote or directory name. +def _get_repo_info(repo_path: Path) -> tuple[str, str, str]: + """Extract host, owner, and repo name from git remote or directory name. Returns: - Tuple of (owner, repo_name) + Tuple of (host, owner, repo_name). ``host`` is empty when it cannot + be determined (e.g. no ``origin`` remote configured). """ try: remote_url = _run_git(repo_path, "remote", "get-url", "origin") # Handle SSH: git@github.com:owner/repo.git if remote_url.startswith("git@"): - path_part = remote_url.split(":", 1)[1] + host_part, path_part = remote_url.split(":", 1) + host = host_part.split("@", 1)[-1] # Handle HTTPS: https://github.com/owner/repo.git elif "://" in remote_url: - path_part = remote_url.split("://", 1)[1].split("/", 1)[1] + after_scheme = remote_url.split("://", 1)[1] + host, path_part = after_scheme.split("/", 1) + # Strip any embedded credentials (e.g. user@host or token@host) + host = host.split("@")[-1] else: + host = "" path_part = remote_url # Remove .git suffix path_part = path_part.removesuffix(".git") parts = path_part.strip("/").split("/") if len(parts) >= 2: - return parts[-2], parts[-1] + return host, parts[-2], parts[-1] except (RuntimeError, IndexError, ValueError): pass - # Fallback to directory name - return "local", repo_path.name + # Fallback to directory name, no known host + return "", "local", repo_path.name def _get_current_branch(repo_path: Path) -> str: @@ -118,7 +124,7 @@ def build_mr_from_diff( if not (repo_path / ".git").exists(): raise FileNotFoundError(f"Not a git repository: {repo_path}") - owner, repo_name = _get_repo_info(repo_path) + host, owner, repo_name = _get_repo_info(repo_path) head_branch = _get_current_branch(repo_path) author = _get_current_user(repo_path) @@ -161,6 +167,7 @@ def build_mr_from_diff( updated_at=datetime.utcnow(), repo_owner=owner, repo_name=repo_name, + host=host, changed_files=[], platform=GitPlatform.GITHUB, # Doesn't matter for local review ) @@ -214,6 +221,7 @@ def build_mr_from_diff( updated_at=datetime.utcnow(), repo_owner=owner, repo_name=repo_name, + host=host, changed_files=changed_files, platform=GitPlatform.GITHUB, # Doesn't matter for local review ) diff --git a/src/codespy/tools/git/models.py b/src/codespy/tools/git/models.py index 6b397fe..667b6c1 100644 --- a/src/codespy/tools/git/models.py +++ b/src/codespy/tools/git/models.py @@ -239,6 +239,13 @@ class MergeRequest(BaseModel): updated_at: datetime = Field(description="MR/PR last update timestamp") repo_owner: str = Field(description="Repository owner/namespace") repo_name: str = Field(description="Repository name") + host: str = Field( + default="", + description=( + "Repo host (e.g. 'github.com', 'gitlab.example.com'). " + "Empty when unknown (e.g. local repo with no git remote)." + ), + ) changed_files: list[ChangedFile] = Field( default_factory=list, description="List of changed files" ) @@ -250,6 +257,18 @@ def repo_full_name(self) -> str: """Get full repository name (owner/repo).""" return f"{self.repo_owner}/{self.repo_name}" + @property + def repo_slug(self) -> str: + """Get the host-qualified repository identifier (e.g. 'github.com/owner/repo'). + + Falls back to ``repo_full_name`` (no host prefix) when ``host`` is + unknown — e.g. a local repository with no git remote configured. + Used by Hippocampus memory to build a stable, host-qualified episode + path so local and remote reviews of the same repository share memory. + """ + base = self.repo_full_name + return f"{self.host}/{base}" if self.host else base + @property def url(self) -> str: """Get the MR/PR URL.""" @@ -344,4 +363,4 @@ def get_callers_for_file(self, filename: str) -> str: if len(func_callers) > 10: lines.append(f" ... and {len(func_callers) - 10} more callers") - return "\n".join(lines) \ No newline at end of file + return "\n".join(lines) From 6510c81e2f1efa9d42da8601172b88e757591324 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 14:09:34 +0200 Subject: [PATCH 14/79] wip --- src/codespy/agents/memory/hippocampus/hypocampus.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/hypocampus.py b/src/codespy/agents/memory/hippocampus/hypocampus.py index 3bfdc2c..826b3e4 100644 --- a/src/codespy/agents/memory/hippocampus/hypocampus.py +++ b/src/codespy/agents/memory/hippocampus/hypocampus.py @@ -2,6 +2,7 @@ import asyncio import copy +import uuid from datetime import datetime import dspy @@ -247,16 +248,17 @@ def _finalize_episode(self) -> None: def _episode_file_path(self, dir: str) -> str: """Build the full episode file path from a directory. - Prepends the ``episodes`` root and appends a timestamped filename - named after the wrapped task: ``episodes//codespy--.json``. + Prepends the ``episodes`` root and appends a hidden ``.codespy`` + folder holding the episode file, named after the wrapped task and a + random UUID: ``episodes//.codespy/-.json``. Args: dir: Directory identifying where this episode belongs (e.g. a scope's ``/{repo}/{subroot}/`` path). """ - timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + file_id = uuid.uuid4().hex trimmed = dir.strip("/") - return f"episodes/{trimmed}/codespy-{self._task_name}-{timestamp}.json" + return f"episodes/{trimmed}/.codespy/{self._task_name}-{file_id}.json" def end_episode( self, @@ -277,7 +279,7 @@ def end_episode( If both ``store`` and ``dir`` are provided the episode is persisted via ``save_episode()`` after consolidation, at - ``episodes//codespy--.json``. ``store`` may be a + ``episodes//.codespy/-.json``. ``store`` may be a ``FileSystem`` or an ``S3Client`` instance. Args: From efaa08cf50cdc8c98cf087dc38e30a9022047d54 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 14:17:33 +0200 Subject: [PATCH 15/79] typo --- src/codespy/agents/memory/hippocampus/__init__.py | 4 ++-- src/codespy/agents/memory/hippocampus/budget.py | 2 +- src/codespy/agents/memory/hippocampus/episode.py | 4 ++-- .../hippocampus/{hypocampus.py => hippocampus.py} | 12 ++++++------ src/codespy/agents/reviewer/modules/code_reviewer.py | 4 ++-- src/codespy/agents/reviewer/modules/doc_reviewer.py | 4 ++-- .../agents/reviewer/modules/scope_identifier.py | 4 ++-- .../agents/reviewer/modules/supply_chain_auditor.py | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) rename src/codespy/agents/memory/hippocampus/{hypocampus.py => hippocampus.py} (98%) diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index 88f955d..79a13f4 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -8,7 +8,7 @@ SectionName, ) from codespy.agents.memory.hippocampus.episode import Episode -from codespy.agents.memory.hippocampus.hypocampus import Hypocampus +from codespy.agents.memory.hippocampus.hippocampus import Hippocampus from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig @@ -20,7 +20,7 @@ "Distiller", "DistillerSig", "Episode", - "Hypocampus", + "Hippocampus", "Item", "ItemTag", "Operation", diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 004a62c..cb6a558 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -34,7 +34,7 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: All fields are included in full. If max_tokens is set, the joined result is head+tail bounded via _head_tail_text so both the instruction and any - trailing intent survive. See Hypocampus.max_input_tokens for guidance on + trailing intent survive. See Hippocampus.max_input_tokens for guidance on when and how to set a limit. """ parts: list[str] = [] diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index f154cfa..4392924 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -1,4 +1,4 @@ -"""Episode record and persistence helpers for Hypocampus.""" +"""Episode record and persistence helpers for Hippocampus.""" from __future__ import annotations @@ -13,7 +13,7 @@ class Episode(BaseModel): """A snapshot of an agent's consolidated memory at the end of an episode. - Recorded by ``Hypocampus.end_episode()`` after the buffered trajectories + Recorded by ``Hippocampus.end_episode()`` after the buffered trajectories have been distilled into the context map. It captures *what the agent knew* (the consolidated ``ContextMap``) together with lightweight identity and timing metadata, so a review/run leaves behind a durable, inspectable diff --git a/src/codespy/agents/memory/hippocampus/hypocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py similarity index 98% rename from src/codespy/agents/memory/hippocampus/hypocampus.py rename to src/codespy/agents/memory/hippocampus/hippocampus.py index 826b3e4..7ff1c4a 100644 --- a/src/codespy/agents/memory/hippocampus/hypocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -33,7 +33,7 @@ def prepend_context_map(sig): ) -class Hypocampus(dspy.Module): +class Hippocampus(dspy.Module): """Wraps a dspy.Module with a context map that evolves via LLM-driven reflection. The context map is prepended to every agent call so the agent starts each run @@ -58,28 +58,28 @@ class Hypocampus(dspy.Module): Common patterns:: # Classic per-call (default) — reflect after every call, no end consolidation - mem = Hypocampus(agent) + mem = Hippocampus(agent) pred = mem(task="…") # Pure batch — no online reflection, one holistic pass at the end - mem = Hypocampus(agent, max_reflects=0) + mem = Hippocampus(agent, max_reflects=0) for task in tasks: pred = mem(task=task) mem.end_episode() # Hybrid — warm up online for the first 3 calls, then holistic consolidation - mem = Hypocampus(agent, max_reflects=3) + mem = Hippocampus(agent, max_reflects=3) for task in tasks: pred = mem(task=task) mem.end_episode() # Read-only (map never changes) — pure inference - mem = Hypocampus(agent, max_reflects=0) + mem = Hippocampus(agent, max_reflects=0) pred = mem(task="…") # no end_episode() call # Async variants (for callers running inside an event loop, e.g. # reviewer modules using `await agent.acall(...)`) - mem = Hypocampus(agent, max_reflects=0) + mem = Hippocampus(agent, max_reflects=0) pred = await mem.acall(task="…") await mem.aend_episode(store, dir) diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 7b32dcc..eb96586 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -232,7 +232,7 @@ async def aforward( ) async with SignatureContext("code_review", self._cost_tracker): if self._settings.get_memory_enabled("code_review"): - mem = Hypocampus( + mem = Hippocampus( agent, token_budget=self._settings.get_memory_token_budget("code_review"), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index c990f9a..87e51b2 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( @@ -164,7 +164,7 @@ async def aforward( ) async with SignatureContext("doc", self._cost_tracker): if self._settings.get_memory_enabled("doc"): - mem = Hypocampus( + mem = Hippocampus( reviewer, token_budget=self._settings.get_memory_token_budget("doc"), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 81506e0..f27665d 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType from codespy.config import get_settings from codespy.config_memory import get_memory_store @@ -260,7 +260,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # Track scope signature costs async with SignatureContext("scope", self._cost_tracker): if self._settings.get_memory_enabled("scope"): - mem = Hypocampus( + mem = Hippocampus( agent, token_budget=self._settings.get_memory_token_budget("scope"), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index e4bfe3a..eb553a8 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,7 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hypocampus +from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, resolve_scope_root, strip_prefix, restore_repo_paths from codespy.config import get_settings @@ -309,7 +309,7 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list # Track supply_chain signature costs separately async with SignatureContext("supply_chain", self._cost_tracker): if self._settings.get_memory_enabled("supply_chain"): - mem = Hypocampus( + mem = Hippocampus( supply_chain_agent, token_budget=self._settings.get_memory_token_budget( "supply_chain" From 28690ebca2c5d716697ab34bb8874ed1c1f76eaa Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 14:44:50 +0200 Subject: [PATCH 16/79] typo --- .env.example | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.env.example b/.env.example index 373719e..6d5d076 100644 --- a/.env.example +++ b/.env.example @@ -132,6 +132,39 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Enable provider-side prompt caching (reduces latency and costs) # ENABLE_PROMPT_CACHING=true +# ============================================================================= +# Memory (Hippocampus) +# ============================================================================= +# Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) +# consolidate their run into a ContextMap and persist it as an Episode. +# Save-only for now (no loading). Disabled by default per-signature — see +# the per-signature MEMORY_* settings below. +# +# Episodes are written to: +# episodes///codespy--.json +# under MEMORY_ROOT (filesystem) or MEMORY_S3_BUCKET (s3). + +# Storage backend: filesystem or s3 (default: filesystem) +# MEMORY_BACKEND=filesystem + +# Filesystem backend +# MEMORY_ROOT=~/.cache/codespy/memory + +# S3 backend (used when MEMORY_BACKEND=s3) +# MEMORY_S3_BUCKET=my-bucket +# Falls back to AWS_REGION if not set +# MEMORY_S3_REGION=us-east-1 +# For MinIO / S3-compatible endpoints +# MEMORY_S3_ENDPOINT_URL=https://minio.example.com + +# Reflection defaults — overridable per-signature via _MEMORY_* +# MEMORY_DEFAULT_ENABLED=false +# 0 = reflect once at end_episode +# MEMORY_DEFAULT_MAX_REFLECTS=0 +# MEMORY_DEFAULT_TOKEN_BUDGET=1024 +# Unset = full trajectory +# MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS= + # ============================================================================= # Output Settings # ============================================================================= @@ -168,6 +201,10 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - MAX_CONTEXT_SIZE (integer) # - MAX_REASONING_TOKENS (integer) - Limits reasoning verbosity # - TEMPERATURE (float) - Lower = more deterministic output +# - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) +# - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) +# - MEMORY_TOKEN_BUDGET (integer) - Token budget for the persisted ContextMap +# - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection # Examples: # CODE_REVIEW_ENABLED=true @@ -175,15 +212,36 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 # CODE_REVIEW_MAX_REASONING_TOKENS=512 # CODE_REVIEW_TEMPERATURE=0.1 +# CODE_REVIEW_MEMORY_ENABLED=true +# CODE_REVIEW_MEMORY_MAX_REFLECTS=1 +# CODE_REVIEW_MEMORY_TOKEN_BUDGET=1024 +# CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS= # SUPPLY_CHAIN_ENABLED=true +# When true: scans ALL artifacts (Dockerfiles, etc.) and manifests +# When false (default): only scans artifacts/manifests modified in the MR +# SUPPLY_CHAIN_SCAN_UNCHANGED=false +# SUPPLY_CHAIN_MEMORY_ENABLED=true +# SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 +# SUPPLY_CHAIN_MEMORY_TOKEN_BUDGET=1024 +# SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS= # DOC_ENABLED=true # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 +# DOC_MEMORY_ENABLED=true +# DOC_MEMORY_MAX_REFLECTS=1 +# DOC_MEMORY_TOKEN_BUDGET=1024 +# DOC_MEMORY_MAX_TRAJECTORY_TOKENS= # SCOPE_ENABLED=true # SCOPE_MAX_ITERS=10 # SCOPE_MAX_REASONING_TOKENS=1024 +# SCOPE_MEMORY_ENABLED=true +# SCOPE_MEMORY_MAX_REFLECTS=1 +# SCOPE_MEMORY_TOKEN_BUDGET=1024 +# SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS= # SUMMARIZATION_ENABLED=true # SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 +# Memory is not wired for summarization (no tools/scope) — leave disabled +# SUMMARIZATION_MEMORY_ENABLED=false From 7f1897dc5dd53ba98061c4f30830cfe084acad17 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 15:01:43 +0200 Subject: [PATCH 17/79] cfg --- .gitignore | 3 ++ src/codespy/config.py | 10 +++++- src/codespy/config_memory.py | 65 +++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 363215c..0d1448d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ coverage.xml # Environments .env +.env.bak* +.env.*.bak +.env.local .venv env/ venv/ diff --git a/src/codespy/config.py b/src/codespy/config.py index 15d78e8..8592d3f 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -27,7 +27,11 @@ discover_gemini_api_key, discover_openai_api_key, ) -from codespy.config_memory import MemoryConfig, reset_memory_store +from codespy.config_memory import ( + MemoryConfig, + apply_memory_env_overrides, + reset_memory_store, +) logger = logging.getLogger(__name__) @@ -256,6 +260,10 @@ def load_yaml_config(cls, values: dict[str, Any]) -> dict[str, Any]: """ yaml_config = _load_yaml_config() yaml_config = apply_signature_env_overrides(yaml_config) + # MEMORY_* env vars target the nested `memory` model, which + # pydantic-settings cannot populate on its own (no env_nested_delimiter). + yaml_config = apply_memory_env_overrides(yaml_config) + # Merge YAML config into values only if not already set (env vars take precedence) for key, val in yaml_config.items(): diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 9d2341a..94c3ba1 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal +import os +from typing import TYPE_CHECKING, Any, Literal from pydantic import BaseModel, Field @@ -37,6 +38,68 @@ class MemoryConfig(BaseModel): default_max_trajectory_tokens: int | None = None # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS +# Env var name (without the MEMORY_ prefix) -> MemoryConfig field name. +# ``memory`` is a nested model and ``Settings`` does not set +# ``env_nested_delimiter``, so pydantic-settings cannot populate these fields +# from the environment on its own. apply_memory_env_overrides() bridges the gap. +MEMORY_ENV_SETTINGS = { + "BACKEND": "backend", + "ROOT": "root", + "S3_BUCKET": "s3_bucket", + "S3_REGION": "s3_region", + "S3_ENDPOINT_URL": "s3_endpoint_url", + "DEFAULT_ENABLED": "default_enabled", + "DEFAULT_MAX_REFLECTS": "default_max_reflects", + "DEFAULT_TOKEN_BUDGET": "default_token_budget", + "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", +} + + +def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: + """Apply ``MEMORY_*`` environment variable overrides to the ``memory`` block. + + Maps flat env vars onto the nested ``memory`` config, e.g.:: + + MEMORY_BACKEND=s3 -> memory.backend + MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled + MEMORY_DEFAULT_TOKEN_BUDGET=512 -> memory.default_token_budget + + Env vars take precedence over YAML, matching the documented priority + (Environment Variables > YAML Config > Defaults). + + Note: ``_MEMORY_*`` vars are handled separately by + ``apply_signature_env_overrides`` and are ignored here, since they never + match a bare ``MEMORY_`` prefix. + + Args: + config: The YAML-derived config dict to mutate. + + Returns: + The same dict, with ``memory`` overrides applied. + """ + from dotenv import dotenv_values + + from codespy.config_dspy import convert_env_value + + env_vars = {**dotenv_values(".env"), **os.environ} + + for key, value in env_vars.items(): + if value is None: + continue + key_upper = key.upper() + if not key_upper.startswith("MEMORY_"): + continue + field = MEMORY_ENV_SETTINGS.get(key_upper[len("MEMORY_"):]) + if field is None: + continue + memory_config = config.setdefault("memory", {}) + if not isinstance(memory_config, dict): + continue + memory_config[field] = convert_env_value(value) + + return config + + # Cached singleton store. Avoids reconstructing an S3Client's boto3 client # (credential resolution + connection pool setup) on every call — see # get_memory_store() for details. Filesystem stores are cheap to build but From 4389a5f93b2422023233f422dff0bb5fd6acc6e9 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 17:47:32 +0200 Subject: [PATCH 18/79] cfg --- .env.example | 44 +++++-- action.yml | 92 +++++---------- codespy.yaml | 58 +++++++--- src/codespy/agents/cost_tracker.py | 44 ++++--- src/codespy/agents/dspy_config.py | 103 ++++++++++++----- .../hippocampus/modules/cartographer.py | 33 ++++-- .../memory/hippocampus/modules/distiller.py | 19 ++- .../reviewer/modules/scope_identifier.py | 8 +- src/codespy/cli.py | 3 +- src/codespy/config.py | 96 +++++++++++----- src/codespy/config_dspy.py | 38 +++--- src/codespy/config_memory.py | 108 +++++++++++++++++- 12 files changed, 440 insertions(+), 206 deletions(-) diff --git a/.env.example b/.env.example index 6d5d076..057363a 100644 --- a/.env.example +++ b/.env.example @@ -103,9 +103,17 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Cheap (SUMMARIZATION_MODEL): PR summary generation. Simple synthesis. # Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # +# Cheap (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory +# reflection — summarizing a trajectory and curating the context map. +# Compact, frequent tasks. +# Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. +# # By default, all models fall back to DEFAULT_MODEL. To optimize costs: # EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 # SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 + # ============================================================================= # Default Settings @@ -117,11 +125,10 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 # DEFAULT_MAX_ITERS=3 -# DEFAULT_MAX_CONTEXT_SIZE=50000 -# Limits LLM reasoning verbosity (helps prevent JSONAdapter failures) -# DEFAULT_MAX_REASONING_TOKENS=1024 -# Lower = more deterministic JSON output -# DEFAULT_TEMPERATURE=0.1 +# Provider reasoning budget: minimal | low | medium | high +# DEFAULT_REASONING_EFFORT=medium +# Must be 1 while reasoning is enabled (providers reject other values) +# DEFAULT_TEMPERATURE=1 # Global LLM reliability settings # Number of retries for LLM API calls @@ -165,6 +172,21 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Unset = full trajectory # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS= +# LLM settings for the reflection modules. The Distiller summarizes a trajectory; +# the Cartographer curates the context map. Both are compact summarize/curate +# tasks, so a cheaper tier than code review usually suffices. +# Each falls back to the corresponding DEFAULT_* value when unset. +# MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_DISTILLER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_DISTILLER_REASONING_EFFORT=low +# MEMORY_DISTILLER_TEMPERATURE=1 + +# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_CARTOGRAPHER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_CARTOGRAPHER_REASONING_EFFORT=low +# MEMORY_CARTOGRAPHER_TEMPERATURE=1 + + # ============================================================================= # Output Settings # ============================================================================= @@ -198,9 +220,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - ENABLED (true/false) # - MAX_ITERS (integer) # - MODEL (LiteLLM model string) -# - MAX_CONTEXT_SIZE (integer) -# - MAX_REASONING_TOKENS (integer) - Limits reasoning verbosity -# - TEMPERATURE (float) - Lower = more deterministic output +# - REASONING_EFFORT (minimal|low|medium|high) - Provider reasoning budget +# - TEMPERATURE (float) - Must be 1 while reasoning is enabled # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) # - MEMORY_TOKEN_BUDGET (integer) - Token budget for the persisted ContextMap @@ -210,8 +231,9 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_ENABLED=true # CODE_REVIEW_MAX_ITERS=10 # CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 -# CODE_REVIEW_MAX_REASONING_TOKENS=512 -# CODE_REVIEW_TEMPERATURE=0.1 +# CODE_REVIEW_REASONING_EFFORT=high +# CODE_REVIEW_TEMPERATURE=1 + # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 # CODE_REVIEW_MEMORY_TOKEN_BUDGET=1024 @@ -235,7 +257,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SCOPE_ENABLED=true # SCOPE_MAX_ITERS=10 -# SCOPE_MAX_REASONING_TOKENS=1024 +# SCOPE_REASONING_EFFORT=low # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 # SCOPE_MEMORY_TOKEN_BUDGET=1024 diff --git a/action.yml b/action.yml index a542616..f8e3583 100644 --- a/action.yml +++ b/action.yml @@ -68,20 +68,16 @@ inputs: required: false default: '3' - default-max-context-size: - description: 'Default maximum context size for all signatures' + default-reasoning-effort: + description: 'Default reasoning effort for all signatures (minimal|low|medium|high)' required: false - default: '50000' - - default-max-reasoning-tokens: - description: 'Default maximum reasoning tokens for all signatures' - required: false - default: '8000' + default: 'medium' default-temperature: - description: 'Default temperature for LLM calls (0.0-1.0)' + description: 'Default temperature for LLM calls (must be 1 while reasoning is enabled)' required: false - default: '0.1' + default: '1' + llm-retries: description: 'Number of retries for LLM API calls' @@ -114,12 +110,8 @@ inputs: description: 'Max iterations for scope identification' required: false - scope-max-context-size: - description: 'Max context size for scope identification' - required: false - - scope-max-reasoning-tokens: - description: 'Max reasoning tokens for scope identification' + scope-reasoning-effort: + description: 'Reasoning effort for scope (minimal|low|medium|high)' required: false scope-temperature: @@ -142,12 +134,8 @@ inputs: description: 'Max iterations for code review' required: false - code-review-max-context-size: - description: 'Max context size for code review' - required: false - - code-review-max-reasoning-tokens: - description: 'Max reasoning tokens for code review' + code-review-reasoning-effort: + description: 'Reasoning effort for code review (minimal|low|medium|high)' required: false code-review-temperature: @@ -170,12 +158,8 @@ inputs: description: 'Max iterations for doc review' required: false - doc-max-context-size: - description: 'Max context size for doc review' - required: false - - doc-max-reasoning-tokens: - description: 'Max reasoning tokens for doc review' + doc-reasoning-effort: + description: 'Reasoning effort for doc (minimal|low|medium|high)' required: false doc-temperature: @@ -198,12 +182,8 @@ inputs: description: 'Max iterations for supply chain security' required: false - supply-chain-max-context-size: - description: 'Max context size for supply chain security' - required: false - - supply-chain-max-reasoning-tokens: - description: 'Max reasoning tokens for supply chain security' + supply-chain-reasoning-effort: + description: 'Reasoning effort for supply chain (minimal|low|medium|high)' required: false supply-chain-temperature: @@ -231,12 +211,8 @@ inputs: description: 'Max iterations for summarization' required: false - summarization-max-context-size: - description: 'Max context size for summarization' - required: false - - summarization-max-reasoning-tokens: - description: 'Max reasoning tokens for summarization' + summarization-reasoning-effort: + description: 'Reasoning effort for summarization (minimal|low|medium|high)' required: false summarization-temperature: @@ -309,8 +285,7 @@ runs: # Global defaults DEFAULT_MAX_ITERS: ${{ inputs.default-max-iters }} - DEFAULT_MAX_CONTEXT_SIZE: ${{ inputs.default-max-context-size }} - DEFAULT_MAX_REASONING_TOKENS: ${{ inputs.default-max-reasoning-tokens }} + DEFAULT_REASONING_EFFORT: ${{ inputs.default-reasoning-effort }} DEFAULT_TEMPERATURE: ${{ inputs.default-temperature }} LLM_RETRIES: ${{ inputs.llm-retries }} LLM_TIMEOUT: ${{ inputs.llm-timeout }} @@ -320,32 +295,28 @@ runs: SCOPE_ENABLED: ${{ inputs.scope-enabled }} SCOPE_MODEL: ${{ inputs.scope-model }} SCOPE_MAX_ITERS: ${{ inputs.scope-max-iters }} - SCOPE_MAX_CONTEXT_SIZE: ${{ inputs.scope-max-context-size }} - SCOPE_MAX_REASONING_TOKENS: ${{ inputs.scope-max-reasoning-tokens }} + SCOPE_REASONING_EFFORT: ${{ inputs.scope-reasoning-effort }} SCOPE_TEMPERATURE: ${{ inputs.scope-temperature }} # Code review signature CODE_REVIEW_ENABLED: ${{ inputs.code-review-enabled }} CODE_REVIEW_MODEL: ${{ inputs.code-review-model }} CODE_REVIEW_MAX_ITERS: ${{ inputs.code-review-max-iters }} - CODE_REVIEW_MAX_CONTEXT_SIZE: ${{ inputs.code-review-max-context-size }} - CODE_REVIEW_MAX_REASONING_TOKENS: ${{ inputs.code-review-max-reasoning-tokens }} + CODE_REVIEW_REASONING_EFFORT: ${{ inputs.code-review-reasoning-effort }} CODE_REVIEW_TEMPERATURE: ${{ inputs.code-review-temperature }} # Doc review signature DOC_ENABLED: ${{ inputs.doc-enabled }} DOC_MODEL: ${{ inputs.doc-model }} DOC_MAX_ITERS: ${{ inputs.doc-max-iters }} - DOC_MAX_CONTEXT_SIZE: ${{ inputs.doc-max-context-size }} - DOC_MAX_REASONING_TOKENS: ${{ inputs.doc-max-reasoning-tokens }} + DOC_REASONING_EFFORT: ${{ inputs.doc-reasoning-effort }} DOC_TEMPERATURE: ${{ inputs.doc-temperature }} # Supply chain signature SUPPLY_CHAIN_ENABLED: ${{ inputs.supply-chain-enabled }} SUPPLY_CHAIN_MODEL: ${{ inputs.supply-chain-model }} SUPPLY_CHAIN_MAX_ITERS: ${{ inputs.supply-chain-max-iters }} - SUPPLY_CHAIN_MAX_CONTEXT_SIZE: ${{ inputs.supply-chain-max-context-size }} - SUPPLY_CHAIN_MAX_REASONING_TOKENS: ${{ inputs.supply-chain-max-reasoning-tokens }} + SUPPLY_CHAIN_REASONING_EFFORT: ${{ inputs.supply-chain-reasoning-effort }} SUPPLY_CHAIN_TEMPERATURE: ${{ inputs.supply-chain-temperature }} SUPPLY_CHAIN_SCAN_UNCHANGED: ${{ inputs.supply-chain-scan-unchanged }} @@ -353,8 +324,7 @@ runs: SUMMARIZATION_ENABLED: ${{ inputs.summarization-enabled }} SUMMARIZATION_MODEL: ${{ inputs.summarization-model }} SUMMARIZATION_MAX_ITERS: ${{ inputs.summarization-max-iters }} - SUMMARIZATION_MAX_CONTEXT_SIZE: ${{ inputs.summarization-max-context-size }} - SUMMARIZATION_MAX_REASONING_TOKENS: ${{ inputs.summarization-max-reasoning-tokens }} + SUMMARIZATION_REASONING_EFFORT: ${{ inputs.summarization-reasoning-effort }} SUMMARIZATION_TEMPERATURE: ${{ inputs.summarization-temperature }} # Other settings @@ -377,8 +347,7 @@ runs: # Global defaults [ -n "$DEFAULT_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_ITERS" - [ -n "$DEFAULT_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_CONTEXT_SIZE" - [ -n "$DEFAULT_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_REASONING_TOKENS" + [ -n "$DEFAULT_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_REASONING_EFFORT" [ -n "$DEFAULT_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_TEMPERATURE" [ -n "$LLM_RETRIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e LLM_RETRIES" [ -n "$LLM_TIMEOUT" ] && DOCKER_ARGS="$DOCKER_ARGS -e LLM_TIMEOUT" @@ -388,32 +357,28 @@ runs: [ -n "$SCOPE_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_ENABLED" [ -n "$SCOPE_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MODEL" [ -n "$SCOPE_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MAX_ITERS" - [ -n "$SCOPE_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MAX_CONTEXT_SIZE" - [ -n "$SCOPE_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MAX_REASONING_TOKENS" + [ -n "$SCOPE_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_REASONING_EFFORT" [ -n "$SCOPE_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_TEMPERATURE" # Code review [ -n "$CODE_REVIEW_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_ENABLED" [ -n "$CODE_REVIEW_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MODEL" [ -n "$CODE_REVIEW_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MAX_ITERS" - [ -n "$CODE_REVIEW_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MAX_CONTEXT_SIZE" - [ -n "$CODE_REVIEW_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MAX_REASONING_TOKENS" + [ -n "$CODE_REVIEW_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_REASONING_EFFORT" [ -n "$CODE_REVIEW_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_TEMPERATURE" # Doc review [ -n "$DOC_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_ENABLED" [ -n "$DOC_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MODEL" [ -n "$DOC_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MAX_ITERS" - [ -n "$DOC_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MAX_CONTEXT_SIZE" - [ -n "$DOC_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MAX_REASONING_TOKENS" + [ -n "$DOC_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_REASONING_EFFORT" [ -n "$DOC_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_TEMPERATURE" # Supply chain [ -n "$SUPPLY_CHAIN_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_ENABLED" [ -n "$SUPPLY_CHAIN_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MODEL" [ -n "$SUPPLY_CHAIN_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MAX_ITERS" - [ -n "$SUPPLY_CHAIN_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MAX_CONTEXT_SIZE" - [ -n "$SUPPLY_CHAIN_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MAX_REASONING_TOKENS" + [ -n "$SUPPLY_CHAIN_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_REASONING_EFFORT" [ -n "$SUPPLY_CHAIN_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_TEMPERATURE" [ -n "$SUPPLY_CHAIN_SCAN_UNCHANGED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_SCAN_UNCHANGED" @@ -421,8 +386,7 @@ runs: [ -n "$SUMMARIZATION_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_ENABLED" [ -n "$SUMMARIZATION_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MODEL" [ -n "$SUMMARIZATION_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MAX_ITERS" - [ -n "$SUMMARIZATION_MAX_CONTEXT_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MAX_CONTEXT_SIZE" - [ -n "$SUMMARIZATION_MAX_REASONING_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MAX_REASONING_TOKENS" + [ -n "$SUMMARIZATION_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_REASONING_EFFORT" [ -n "$SUMMARIZATION_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_TEMPERATURE" # Other settings diff --git a/codespy.yaml b/codespy.yaml index 9c24401..4b7dc42 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -85,12 +85,29 @@ memory: default_token_budget: 1024 # MEMORY_DEFAULT_TOKEN_BUDGET default_max_trajectory_tokens: null # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS (null = full trajectory) + # LLM settings for the reflection modules. The Distiller summarizes a + # trajectory; the Cartographer curates the context map. Both are compact + # summarize/curate tasks, so a cheaper tier than code review usually suffices. + # null inherits the top-level default_* value. + distiller: + model: null # MEMORY_DISTILLER_MODEL + extraction_model: null # MEMORY_DISTILLER_EXTRACTION_MODEL + reasoning_effort: null # MEMORY_DISTILLER_REASONING_EFFORT + temperature: null # MEMORY_DISTILLER_TEMPERATURE + cartographer: + model: null # MEMORY_CARTOGRAPHER_MODEL + extraction_model: null # MEMORY_CARTOGRAPHER_EXTRACTION_MODEL + reasoning_effort: null # MEMORY_CARTOGRAPHER_REASONING_EFFORT + temperature: null # MEMORY_CARTOGRAPHER_TEMPERATURE + + # ============================================================================ # SIGNATURES # ============================================================================ -# Each signature config supports: enabled, max_iters, model, max_context_size -# Set to null to use defaults +# Each signature config supports: enabled, max_iters, model, reasoning_effort, +# temperature. Set to null to inherit the corresponding default_* value. + # # RECOMMENDED MODEL STRATEGY # ============================================================================ @@ -107,23 +124,33 @@ memory: # Cheap (summarization): Used for PR summary generation. Simple synthesis # task. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # -# By default, all models fall back to default_model. Override extraction_model -# and summarization model for cost optimization: +# Cheap (memory.distiller / memory.cartographer): Memory reflection — +# summarizing a trajectory and curating the context map. Compact, frequent +# tasks. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. +# +# By default, all models fall back to default_model. Override extraction_model, +# the summarization model, and the reflection models for cost optimization: # # default_model: anthropic/claude-opus-4-6 # extraction_model: anthropic/claude-sonnet-4-5-20250929 # signatures: # summarization: # model: anthropic/claude-haiku-4-5-20251001 +# memory: +# distiller: +# model: anthropic/claude-haiku-4-5-20251001 +# cartographer: +# model: anthropic/claude-haiku-4-5-20251001 # ============================================================================ -# These apply to all signatures unless overridden per-signature + +# These apply to all signatures and reflection modules unless overridden default_model: anthropic/claude-opus-4-6 # DEFAULT_MODEL extraction_model: null # EXTRACTION_MODEL (falls back to default_model) default_max_iters: 20 # DEFAULT_MAX_ITERS -default_max_context_size: 100000 # DEFAULT_MAX_CONTEXT_SIZE -default_max_reasoning_tokens: 6000 # DEFAULT_MAX_REASONING_TOKENS (limits LLM reasoning verbosity) -default_temperature: 0 # DEFAULT_TEMPERATURE (lower = more deterministic output) +default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) +default_temperature: 1 # DEFAULT_TEMPERATURE (must be 1 while reasoning is enabled) + # Global LLM reliability settings llm_retries: 3 # LLM_RETRIES (number of retries for LLM API calls) @@ -135,8 +162,7 @@ signatures: enabled: true # SUPPLY_CHAIN_ENABLED max_iters: null # SUPPLY_CHAIN_MAX_ITERS model: null # SUPPLY_CHAIN_MODEL (Haiku 4.5) - max_context_size: null # SUPPLY_CHAIN_MAX_CONTEXT_SIZE - max_reasoning_tokens: null # SUPPLY_CHAIN_MAX_REASONING_TOKENS + reasoning_effort: null # SUPPLY_CHAIN_REASONING_EFFORT temperature: null # SUPPLY_CHAIN_TEMPERATURE scan_unchanged: false # SUPPLY_CHAIN_SCAN_UNCHANGED # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests @@ -153,8 +179,7 @@ signatures: enabled: true # CODE_REVIEW_ENABLED max_iters: null # CODE_REVIEW_MAX_ITERS model: null # CODE_REVIEW_MODEL - max_context_size: null # CODE_REVIEW_MAX_CONTEXT_SIZE - max_reasoning_tokens: null # CODE_REVIEW_MAX_REASONING_TOKENS + reasoning_effort: null # CODE_REVIEW_REASONING_EFFORT temperature: null # CODE_REVIEW_TEMPERATURE memory: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) @@ -167,8 +192,7 @@ signatures: doc: enabled: true # DOC_ENABLED model: null # DOC_MODEL - max_context_size: null # DOC_MAX_CONTEXT_SIZE - max_reasoning_tokens: null # DOC_MAX_REASONING_TOKENS + reasoning_effort: null # DOC_REASONING_EFFORT temperature: null # DOC_TEMPERATURE memory: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) @@ -181,8 +205,7 @@ signatures: enabled: true # SCOPE_ENABLED max_iters: null # SCOPE_MAX_ITERS model: null # SCOPE_MODEL - max_context_size: null # SCOPE_MAX_CONTEXT_SIZE - max_reasoning_tokens: null # SCOPE_MAX_REASONING_TOKENS + reasoning_effort: null # SCOPE_REASONING_EFFORT temperature: null # SCOPE_TEMPERATURE memory: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) @@ -194,8 +217,7 @@ signatures: summarization: enabled: true # SUMMARIZATION_ENABLED model: null # SUMMARIZATION_MODEL (falls back to default_model) - max_context_size: null # SUMMARIZATION_MAX_CONTEXT_SIZE - max_reasoning_tokens: null # SUMMARIZATION_MAX_REASONING_TOKENS + reasoning_effort: null # SUMMARIZATION_REASONING_EFFORT temperature: null # SUMMARIZATION_TEMPERATURE memory: enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index a8350fc..60a797a 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -197,20 +197,23 @@ def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) class SignatureContext: - """Context manager for tracking signature execution. - - Uses DSPy's LM history mechanism to track costs reliably, even during - parallel execution with dspy.Parallel. Works by: - 1. Recording history UUIDs before signature execution - 2. After execution, finding new entries (by UUID) - 3. Summing costs/tokens from new entries - + """Context manager scoping one named unit of LLM work. + + Two responsibilities, both keyed off the same name: + + 1. **LM selection** — applies the model, temperature, and reasoning effort + configured for this name (``signatures.`` or ``memory.``), + falling back to the top-level defaults. + 2. **Cost attribution** — uses DSPy's LM history to attribute costs + reliably, even during parallel execution with dspy.Parallel, by + recording history UUIDs on entry and summing only the new entries. + Usage: with SignatureContext("code_review", cost_tracker): - # All LLM calls here will be attributed to code_review + # Runs on code_review's configured model, costs attributed to it result = await agent.acall(...) """ - + def __init__(self, signature_name: str, tracker: "CostTracker") -> None: """Initialize the signature context. @@ -221,22 +224,35 @@ def __init__(self, signature_name: str, tracker: "CostTracker") -> None: self.signature_name = signature_name self.tracker = tracker self._before_uuids: set[str] = set() + self._lm_context = None def __enter__(self) -> "SignatureContext": - """Enter the context, capturing current history state.""" - # Capture UUIDs of entries that exist before signature execution + """Enter the context, applying the LM and capturing history state.""" + # Imported here to avoid a circular import at module load time + # (dspy_config imports codespy.config, which must not import agents). + from codespy.agents.dspy_config import lm_context + + # Apply this name's LM first, so the history we snapshot below belongs + # to the LM that will actually serve the enclosed calls. + self._lm_context = lm_context(self.signature_name) + self._lm_context.__enter__() self._before_uuids = _get_history_uuids() self.tracker.start_signature(self.signature_name) return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: """Exit the context, calculating costs from new history entries.""" - # Get all current entries and calculate costs from new ones + # Read history before leaving the LM context, so dspy.settings.lm still + # points at the LM whose history we need. entries = _get_history_entries() cost, tokens, call_count = _calculate_costs_from_entries(entries, self._before_uuids) - self.tracker.end_signature(self.signature_name, cost, tokens, call_count) + if self._lm_context is not None: + self._lm_context.__exit__(exc_type, exc_val, exc_tb) + self._lm_context = None + + async def __aenter__(self) -> "SignatureContext": """Async enter the context.""" return self.__enter__() diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 66f3ea8..312491b 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -6,12 +6,66 @@ from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] -from codespy.config import Settings +from codespy.config import Settings, get_settings +from codespy.config_memory import LLMSettings, REFLECTION_MODULES + logger = logging.getLogger(__name__) +def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: + """Build a ``dspy.LM`` for resolved LLM settings. + + The single place LMs are constructed, so the cross-cutting concerns + (timeout, retries, provider-side prompt caching) are applied uniformly. + + ``reasoning_effort`` is forwarded to LiteLLM through ``dspy.LM``'s + ``**kwargs``; LiteLLM maps it onto each provider's native parameter + (Anthropic thinking budget, OpenAI reasoning effort, ...). + + Args: + settings: Application settings (timeout / retries / prompt caching). + config: Resolved settings from ``Settings.get_llm_config()``. + + Returns: + A configured ``dspy.LM``. + """ + lm_kwargs: dict = { + "model": config.model, + "temperature": config.temperature, + "reasoning_effort": config.reasoning_effort, + "timeout": settings.llm_timeout, + "num_retries": settings.llm_retries, + } + # Cache system prompts on the provider's servers (Anthropic, OpenAI, Bedrock...) + if settings.enable_prompt_caching: + lm_kwargs["cache_control_injection_points"] = [ + {"location": "message", "role": "system"} + ] + return dspy.LM(**lm_kwargs) + + +def lm_context(name: str): + """Return a ``dspy.context`` applying the LM configured for ``name``. + + ``name`` is a signature or reflection module name — see + ``Settings.get_llm_config``. Enter this around the predictor call so the + named unit of work runs on its own configured model, falling back to the + top-level defaults when it declares no overrides. + + Args: + name: The signature or reflection module name. + + Returns: + A context manager that scopes the LM to the enclosed block. + """ + settings = get_settings() + return dspy.context(lm=new_lm(settings, settings.get_llm_config(name))) + + + def configure_dspy(settings: Settings) -> None: + """Configure DSPy with the LLM backend for reliable structured output. This configures DSPy with: @@ -44,33 +98,19 @@ def configure_dspy(settings: Settings) -> None: if settings.aws_secret_access_key: os.environ["AWS_SECRET_ACCESS_KEY"] = settings.aws_secret_access_key - # Build LM kwargs with reliability settings - lm_kwargs: dict = { - "model": model, - "timeout": settings.llm_timeout, # Global timeout (default: 120s) - "num_retries": settings.llm_retries, # Global retries (default: 3) - } + # Global fallback LM, used by any predictor not wrapped in lm_context(). + # An unknown name resolves purely from the top-level defaults. + defaults = settings.get_llm_config("default") + lm = new_lm(settings, defaults) - # Enable provider-side prompt caching if configured - # This caches system prompts on the LLM provider's servers (Anthropic, OpenAI, Bedrock, etc.) - if settings.enable_prompt_caching: - lm_kwargs["cache_control_injection_points"] = [ - {"location": "message", "role": "system"} - ] - - # Configure DSPy with LiteLLM and TwoStepAdapter - lm = dspy.LM(**lm_kwargs) - - # Create extraction LM for TwoStepAdapter's second stage - # Uses a smaller/faster model to extract structured fields from free-form responses - # Falls back to default_model if no extraction_model is configured - extraction_model = settings.extraction_model or settings.default_model - extraction_lm = dspy.LM( - model=extraction_model, - timeout=settings.llm_timeout, - num_retries=settings.llm_retries, + # Extraction LM for TwoStepAdapter's second stage: a smaller/faster model + # that pulls structured fields out of the main LM's free-form response. + extraction_model = defaults.extraction_model + extraction_lm = new_lm( + settings, defaults.model_copy(update={"model": extraction_model}) ) + dspy.settings.configure( lm=lm, adapter=TwoStepAdapter(extraction_lm), # TwoStepAdapter solves ChatAdapter parsing failures @@ -91,7 +131,9 @@ def configure_dspy(settings: Settings) -> None: def verify_model_access(settings: Settings) -> tuple[bool, str]: """Verify that all configured models are accessible. - Checks the default model and all per-signature model overrides. + Checks the default model, all per-signature model overrides, and the + memory reflection models, so a typo in any of them fails fast at startup + rather than mid-review. Args: settings: Application settings containing model configuration. @@ -106,7 +148,14 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: for sig_name, sig_config in settings.signatures.items(): if sig_config.model: models_to_check.add(sig_config.model) - + + # Check the Hippocampus reflection models (Distiller / Cartographer) + for module in REFLECTION_MODULES: + reflection = settings.get_llm_config(module) + models_to_check.add(reflection.model) + models_to_check.add(reflection.extraction_model) + + # Check each model verified: list[str] = [] failed: list[str] = [] diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index 8ab0a0c..dc6d921 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -111,10 +111,11 @@ class CartographerSig(dspy.Signature): token_budget: int = dspy.InputField(desc="Hard token budget for the context map.") current_tokens: int = dspy.InputField(desc="Current token count of the context map.") - reasoning: str = dspy.OutputField( + justification: str = dspy.OutputField( desc="Brief explanation of why these edits improve the shared understanding " "cached in the context map." ) + operations: list[Operation] = dspy.OutputField( desc="Ordered list of ADD/DELETE/REPLACE ops to apply. Empty if nothing " "is worth changing." @@ -130,18 +131,28 @@ class Cartographer(dspy.Module): enforcement is the Evictor's job. """ + # Name this module's settings live under: memory.cartographer. + SIGNATURE = "cartographer" + def __init__(self): super().__init__() - self.predict = dspy.Predict(CartographerSig) + self.predict = dspy.ChainOfThought(CartographerSig) def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, token_budget, current_tokens): - return self.predict( - diagnosis=diagnosis, - item_tags=item_tags, - cache_candidates=cache_candidates, - current_map=current_map, - question=question, - token_budget=token_budget, - current_tokens=current_tokens, - ) + # See Distiller.forward: SignatureContext applies memory.cartographer's + # LLM settings and gives this module its own cost line. Entered here + # because DSPy's context is thread-scoped and reflection runs in a worker. + from codespy.agents import SignatureContext, get_cost_tracker + + with SignatureContext(self.SIGNATURE, get_cost_tracker()): + return self.predict( + diagnosis=diagnosis, + item_tags=item_tags, + cache_candidates=cache_candidates, + current_map=current_map, + question=question, + token_budget=token_budget, + current_tokens=current_tokens, + ) + diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 640d1b7..f051aa2 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -9,6 +9,7 @@ ) + class DistillerSig(dspy.Signature): """You are an expert analyst reviewing an agent's execution trajectory after it interacted with a long external context to answer a question. @@ -133,9 +134,23 @@ class Distiller(dspy.Module): specific work, tags every existing item, and proposes new candidates. """ + # Name this module's settings live under: memory.distiller. + SIGNATURE = "distiller" + def __init__(self): super().__init__() - self.predict = dspy.Predict(DistillerSig) + self.predict = dspy.ChainOfThought(DistillerSig) def forward(self, trajectory: str, context_map: ContextMap, question: str): - return self.predict(trajectory=trajectory, context_map=context_map, question=question) + # SignatureContext applies memory.distiller's model/temperature/reasoning + # effort and attributes the cost to this module rather than to whichever + # agent triggered the reflection. It must be entered here, not by the + # caller: Hippocampus reflects inside an asyncio.to_thread worker and + # DSPy's context is thread-scoped. + from codespy.agents import SignatureContext, get_cost_tracker + + with SignatureContext(self.SIGNATURE, get_cost_tracker()): + return self.predict( + trajectory=trajectory, context_map=context_map, question=question + ) + diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index f27665d..5f3f990 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -245,12 +245,12 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # Build map from filename to ChangedFile for post-processing changed_files_map: dict[str, ChangedFile] = {f.filename: f for f in reviewable_files} try: - # Get per-signature config + # Get per-signature config. The model, temperature, and reasoning + # effort are applied by SignatureContext("scope") below. max_iters = self._settings.get_max_iters("scope") - temperature = self._settings.get_temperature("scope") - max_reasoning = self._settings.get_max_reasoning_tokens("scope") - + # Create ReAct agent + agent = dspy.ReAct( signature=ScopeIdentifierSignature, tools=tools, diff --git a/src/codespy/cli.py b/src/codespy/cli.py index 7c3677c..e8f68a9 100644 --- a/src/codespy/cli.py +++ b/src/codespy/cli.py @@ -77,7 +77,8 @@ def config( # Show non-sensitive settings console.print(f"[bold]Model:[/bold] {settings.default_model}") console.print(f"[bold]AWS Region:[/bold] {settings.aws_region}") - console.print(f"[bold]Max Context Size:[/bold] {settings.default_max_context_size}") + console.print(f"[bold]Reasoning Effort:[/bold] {settings.default_reasoning_effort}") + console.print(f"[bold]Output Format:[/bold] {settings.output_format}") console.print(f"[bold]Cache Directory:[/bold] {settings.cache_dir}") diff --git a/src/codespy/config.py b/src/codespy/config.py index 8592d3f..c3ec09f 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -8,7 +8,12 @@ from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict -from codespy.config_dspy import SignatureConfig, apply_signature_env_overrides +from codespy.config_dspy import ( + ReasoningEffort, + SignatureConfig, + apply_signature_env_overrides, +) + from codespy.config_git import ( GitHubConfig, GitLabConfig, @@ -28,11 +33,17 @@ discover_openai_api_key, ) from codespy.config_memory import ( + LLMSettings, MemoryConfig, + REFLECTION_MODULES, apply_memory_env_overrides, reset_memory_store, ) + + + + logger = logging.getLogger(__name__) # Custom config path (set via CLI --config flag) @@ -107,9 +118,11 @@ class Settings(BaseSettings): default_model: str = "anthropic/claude-opus-4-6" extraction_model: str | None = None # TwoStepAdapter extraction (falls back to default_model) default_max_iters: int = 3 - default_max_context_size: int = 50000 - default_max_reasoning_tokens: int = 8000 # Limit reasoning verbosity for adapter reliability - default_temperature: float = 0.1 # Lower = more deterministic JSON output + # Provider reasoning budget; LiteLLM maps this to each provider's native parameter. + default_reasoning_effort: ReasoningEffort = "medium" + # Providers require temperature=1 when reasoning is enabled. + default_temperature: float = 1.0 + # Global LLM reliability settings llm_retries: int = 3 # Number of retries for LLM API calls @@ -163,30 +176,49 @@ def is_signature_enabled(self, signature_name: str) -> bool: """Check if a signature is enabled.""" return self.get_signature_config(signature_name).enabled - def get_model(self, signature_name: str) -> str: - """Get model for a signature (signature-specific or default).""" - config = self.get_signature_config(signature_name) - return config.model or self.default_model - def get_max_iters(self, signature_name: str) -> int: """Get max_iters for a signature (signature-specific or default).""" config = self.get_signature_config(signature_name) return config.max_iters or self.default_max_iters - def get_max_context_size(self, signature_name: str) -> int: - """Get max_context_size for a signature (signature-specific or default).""" - config = self.get_signature_config(signature_name) - return config.max_context_size or self.default_max_context_size + def get_llm_config(self, name: str) -> LLMSettings: + """Resolve the LLM settings for one named unit of LLM work. - def get_max_reasoning_tokens(self, signature_name: str) -> int: - """Get max_reasoning_tokens for a signature (signature-specific or default).""" - config = self.get_signature_config(signature_name) - return config.max_reasoning_tokens or self.default_max_reasoning_tokens + ``name`` addresses either a signature or a memory reflection module:: + + "code_review" -> signatures.code_review + "distiller" -> memory.distiller + + Every field falls back to its top-level ``default_*`` counterpart, so + the result has no ``None`` fields and callers never re-apply fallbacks. + + Args: + name: A signature name, or a reflection module name + (see ``REFLECTION_MODULES``). + + Returns: + The fully resolved settings for ``name``. + """ + if name in REFLECTION_MODULES: + config = getattr(self.memory, name) + else: + config = self.get_signature_config(name) + + model = config.model or self.default_model + # Only reflection modules carry their own extraction model; signatures + # share the global one. + module_extraction = getattr(config, "extraction_model", None) + return LLMSettings( + model=model, + extraction_model=module_extraction or self.extraction_model or model, + reasoning_effort=config.reasoning_effort or self.default_reasoning_effort, + temperature=( + config.temperature + if config.temperature is not None + else self.default_temperature + ), + ) - def get_temperature(self, signature_name: str) -> float: - """Get temperature for a signature (signature-specific or default).""" - config = self.get_signature_config(signature_name) - return config.temperature if config.temperature is not None else self.default_temperature def get_scan_unchanged(self, signature_name: str) -> bool: """Get scan_unchanged for a signature (signature-specific, default: False). @@ -234,23 +266,25 @@ def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: ) def log_signature_configs(self) -> None: - """Log all signature configurations.""" + """Log all signature and reflection module LLM configurations.""" logger.info("Signature configurations:") for sig_name, sig_config in self.signatures.items(): status = "enabled" if sig_config.enabled else "disabled" - model = sig_config.model or self.default_model - max_iters = sig_config.max_iters or self.default_max_iters - max_reasoning = sig_config.max_reasoning_tokens or self.default_max_reasoning_tokens - temp = ( - sig_config.temperature - if sig_config.temperature is not None - else self.default_temperature + llm = self.get_llm_config(sig_name) + logger.info( + f" {sig_name}: {status}, model={llm.model}, " + f"max_iters={self.get_max_iters(sig_name)}, " + f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}" ) + for module in REFLECTION_MODULES: + llm = self.get_llm_config(module) logger.info( - f" {sig_name}: {status}, model={model}, max_iters={max_iters}, " - f"max_reasoning_tokens={max_reasoning}, temperature={temp}" + f" {module}: model={llm.model}, " + f"extraction_model={llm.extraction_model}, " + f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}" ) + @model_validator(mode="before") @classmethod def load_yaml_config(cls, values: dict[str, Any]) -> dict[str, Any]: diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index d7b7ffb..bedb941 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -2,14 +2,22 @@ import logging import os -from typing import Any +from typing import Any, Literal + from pydantic import BaseModel, Field logger = logging.getLogger(__name__) +# Reasoning budget hint sent to the provider. LiteLLM normalises this to each +# provider's native parameter (Anthropic thinking budget, OpenAI reasoning +# effort, ...), so it works across every supported model. +ReasoningEffort = Literal["minimal", "low", "medium", "high"] + + class MemorySignatureConfig(BaseModel): + """Per-signature Hippocampus memory overrides. All fields are optional — ``None`` means "use the global memory default" @@ -28,10 +36,10 @@ class SignatureConfig(BaseModel): enabled: bool = True max_iters: int | None = None model: str | None = None - max_context_size: int | None = None - max_reasoning_tokens: int | None = None # Limit reasoning verbosity for JSONAdapter reliability - temperature: float | None = None # Lower = more deterministic JSON output + reasoning_effort: ReasoningEffort | None = None # Provider reasoning budget + temperature: float | None = None # Must be 1 when reasoning is enabled scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests + memory: MemorySignatureConfig = Field(default_factory=MemorySignatureConfig) @@ -47,24 +55,14 @@ class SignatureConfig(BaseModel): # Create uppercase prefixes for matching (e.g., "CODE_REVIEW_", "SUPPLY_CHAIN_") SIGNATURE_PREFIXES = {name.upper() + "_": name for name in SIGNATURE_NAMES} -# Known signature settings for validation -SIGNATURE_SETTINGS = { - "enabled", - "max_iters", - "model", - "max_context_size", - "max_reasoning_tokens", - "temperature", - "scan_unchanged", -} +# Known signature settings for validation, derived from the models so the env +# var routing can never drift from the declared fields. ``memory`` is excluded +# because it is nested and routed via _MEMORY_ instead. +SIGNATURE_SETTINGS = set(SignatureConfig.model_fields) - {"memory"} # Known per-signature memory settings, routed via _MEMORY_ -MEMORY_SIGNATURE_SETTINGS = { - "enabled", - "max_reflects", - "token_budget", - "max_trajectory_tokens", -} +MEMORY_SIGNATURE_SETTINGS = set(MemorySignatureConfig.model_fields) + def convert_env_value(value: str) -> Any: diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 94c3ba1..08f9854 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -7,8 +7,10 @@ from pydantic import BaseModel, Field +from codespy.config_dspy import ReasoningEffort from codespy.tools.storage.base import Storage + if TYPE_CHECKING: from codespy.config import Settings @@ -16,7 +18,42 @@ MemoryBackend = Literal["filesystem", "s3"] + +class ReflectionModuleConfig(BaseModel): + """LLM overrides for a single reflection module (Distiller / Cartographer). + + All fields are optional — ``None`` means "fall back to the corresponding + top-level ``default_*`` setting" (see ``codespy.config.Settings``). + + The reflection modules are compact summarize/curate tasks rather than deep + analysis, so they are good candidates for a cheaper model tier than the + one used for code review. + """ + + model: str | None = None # MEMORY__MODEL + extraction_model: str | None = None # MEMORY__EXTRACTION_MODEL + reasoning_effort: ReasoningEffort | None = None # MEMORY__REASONING_EFFORT + temperature: float | None = None # MEMORY__TEMPERATURE + + +class LLMSettings(BaseModel): + """Fully resolved LLM settings for one named unit of work. + + Produced by ``Settings.get_llm_config()`` for either a signature + (``signatures.``) or a reflection module (``memory.``): every + field is either the name-specific override or the corresponding top-level + default, so consumers never re-apply fallback logic. + """ + + model: str + extraction_model: str + reasoning_effort: ReasoningEffort + temperature: float + + + class MemoryConfig(BaseModel): + """Global memory (Hippocampus) configuration. Controls where episodes are persisted and the default reflection knobs @@ -25,6 +62,7 @@ class MemoryConfig(BaseModel): """ # Storage backend + backend: MemoryBackend = "filesystem" # MEMORY_BACKEND root: str = "~/.cache/codespy/memory" # MEMORY_ROOT (filesystem backend) s3_bucket: str | None = None # MEMORY_S3_BUCKET (s3 backend) @@ -37,6 +75,16 @@ class MemoryConfig(BaseModel): default_token_budget: int = Field(default=1024) # MEMORY_DEFAULT_TOKEN_BUDGET default_max_trajectory_tokens: int | None = None # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS + # Per-module LLM overrides for the reflection pipeline. + # Unset fields fall back to the top-level ``default_*`` settings. + distiller: ReflectionModuleConfig = Field( + default_factory=ReflectionModuleConfig + ) # MEMORY_DISTILLER_* + cartographer: ReflectionModuleConfig = Field( + default_factory=ReflectionModuleConfig + ) # MEMORY_CARTOGRAPHER_* + + # Env var name (without the MEMORY_ prefix) -> MemoryConfig field name. # ``memory`` is a nested model and ``Settings`` does not set @@ -54,6 +102,28 @@ class MemoryConfig(BaseModel): "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", } +# The reflection modules, derived from the MemoryConfig fields that hold a +# ReflectionModuleConfig. Iterate this instead of hardcoding module names so +# adding a new reflection module only requires declaring its field above. +REFLECTION_MODULES: tuple[str, ...] = tuple( + name + for name, field in MemoryConfig.model_fields.items() + if field.annotation is ReflectionModuleConfig +) + +# Env var suffix -> ReflectionModuleConfig field name, routed via +# MEMORY__ (e.g. MEMORY_DISTILLER_MODEL). +REFLECTION_MODULE_ENV_SETTINGS = { + name.upper(): name for name in ReflectionModuleConfig.model_fields +} + +# Env var prefix (after MEMORY_) -> MemoryConfig field holding the nested model. +REFLECTION_MODULE_PREFIXES = { + f"{name.upper()}_": name for name in REFLECTION_MODULES +} + + + def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: """Apply ``MEMORY_*`` environment variable overrides to the ``memory`` block. @@ -64,9 +134,15 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled MEMORY_DEFAULT_TOKEN_BUDGET=512 -> memory.default_token_budget + Reflection module overrides use a second level of nesting:: + + MEMORY_DISTILLER_MODEL=... -> memory.distiller.model + MEMORY_CARTOGRAPHER_TEMPERATURE=0 -> memory.cartographer.temperature + Env vars take precedence over YAML, matching the documented priority (Environment Variables > YAML Config > Defaults). + Note: ``_MEMORY_*`` vars are handled separately by ``apply_signature_env_overrides`` and are ignored here, since they never match a bare ``MEMORY_`` prefix. @@ -89,17 +165,43 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: key_upper = key.upper() if not key_upper.startswith("MEMORY_"): continue - field = MEMORY_ENV_SETTINGS.get(key_upper[len("MEMORY_"):]) - if field is None: - continue + remainder = key_upper[len("MEMORY_"):] + memory_config = config.setdefault("memory", {}) if not isinstance(memory_config, dict): continue + + # Reflection module settings: MEMORY__. Checked before + # the flat lookup, since e.g. MEMORY_DISTILLER_MODEL has no entry in + # MEMORY_ENV_SETTINGS and would otherwise be silently dropped. + module_field = next( + ( + (field, remainder[len(prefix):]) + for prefix, field in REFLECTION_MODULE_PREFIXES.items() + if remainder.startswith(prefix) + ), + None, + ) + if module_field is not None: + field, setting = module_field + module_setting = REFLECTION_MODULE_ENV_SETTINGS.get(setting) + if module_setting is None: + continue + module_config = memory_config.setdefault(field, {}) + if not isinstance(module_config, dict): + continue + module_config[module_setting] = convert_env_value(value) + continue + + field = MEMORY_ENV_SETTINGS.get(remainder) + if field is None: + continue memory_config[field] = convert_env_value(value) return config + # Cached singleton store. Avoids reconstructing an S3Client's boto3 client # (credential resolution + connection pool setup) on every call — see # get_memory_store() for details. Filesystem stores are cheap to build but From 4660bebc150ab3c7775e46ed96ce288e0dbb9cb2 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 18:20:07 +0200 Subject: [PATCH 19/79] cfg --- .env.example | 43 ++++++++---- action.yml | 12 ---- codespy.yaml | 34 ++++++++-- .../agents/memory/hippocampus/budget.py | 60 ++++++++++++++--- .../agents/memory/hippocampus/hippocampus.py | 66 ++++++++++++------- .../agents/reviewer/modules/code_reviewer.py | 9 ++- .../agents/reviewer/modules/doc_reviewer.py | 9 ++- .../reviewer/modules/scope_identifier.py | 6 +- .../reviewer/modules/supply_chain_auditor.py | 11 +++- src/codespy/config.py | 33 ++++++++-- src/codespy/config_dspy.py | 11 ++-- src/codespy/config_memory.py | 32 +++++++-- 12 files changed, 241 insertions(+), 85 deletions(-) diff --git a/.env.example b/.env.example index 057363a..225ba2e 100644 --- a/.env.example +++ b/.env.example @@ -168,9 +168,22 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # MEMORY_DEFAULT_ENABLED=false # 0 = reflect once at end_episode # MEMORY_DEFAULT_MAX_REFLECTS=0 -# MEMORY_DEFAULT_TOKEN_BUDGET=1024 -# Unset = full trajectory -# MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS= +# +# Three independent token budgets, most to least cost-sensitive: +# +# 1. Ceiling on the rendered ContextMap. This is the persisted artifact, and it is +# prepended to every agent iteration, so it is re-sent ~DEFAULT_MAX_ITERS times +# per scope. 1024 holds ~12 items. +# MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=1024 +# 2. Cap on the trajectory fed to the Distiller. Tool-using agents can produce +# 100k+ token trajectories and TwoStepAdapter sends the value twice, so keep this +# to ~5-10% of the reflection model's context window. Unset = full trajectory +# (unbounded — not recommended for tool-using agents). +# MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS=8192 +# 3. Cap on the serialized agent inputs used as the reflection "question". Without +# it, every input field is sent in full (for code review, the complete patch of +# every changed file). Unset = unbounded. +# MEMORY_DEFAULT_MAX_QUESTION_TOKENS=2048 # LLM settings for the reflection modules. The Distiller summarizes a trajectory; # the Cartographer curates the context map. Both are compact summarize/curate @@ -224,8 +237,9 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - TEMPERATURE (float) - Must be 1 while reasoning is enabled # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) -# - MEMORY_TOKEN_BUDGET (integer) - Token budget for the persisted ContextMap +# - MEMORY_MAX_CONTEXT_MAP_TOKENS (integer) - Ceiling on the persisted ContextMap # - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection +# - MEMORY_MAX_QUESTION_TOKENS (integer) - Cap on serialized inputs used as the question # Examples: # CODE_REVIEW_ENABLED=true @@ -236,8 +250,9 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 -# CODE_REVIEW_MEMORY_TOKEN_BUDGET=1024 -# CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS= +# CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 # SUPPLY_CHAIN_ENABLED=true # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests @@ -245,23 +260,27 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_SCAN_UNCHANGED=false # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 -# SUPPLY_CHAIN_MEMORY_TOKEN_BUDGET=1024 -# SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS= +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 # DOC_ENABLED=true # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 -# DOC_MEMORY_TOKEN_BUDGET=1024 -# DOC_MEMORY_MAX_TRAJECTORY_TOKENS= +# DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# DOC_MEMORY_MAX_QUESTION_TOKENS=2048 # SCOPE_ENABLED=true # SCOPE_MAX_ITERS=10 # SCOPE_REASONING_EFFORT=low # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 -# SCOPE_MEMORY_TOKEN_BUDGET=1024 -# SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS= +# SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# Unused: scope uses question_field="mr_title", so no inputs are serialized. +# SCOPE_MEMORY_MAX_QUESTION_TOKENS= # SUMMARIZATION_ENABLED=true # SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 diff --git a/action.yml b/action.yml index f8e3583..8f864ee 100644 --- a/action.yml +++ b/action.yml @@ -154,10 +154,6 @@ inputs: description: 'Model for doc review (empty = use default)' required: false - doc-max-iters: - description: 'Max iterations for doc review' - required: false - doc-reasoning-effort: description: 'Reasoning effort for doc (minimal|low|medium|high)' required: false @@ -207,10 +203,6 @@ inputs: description: 'Model for summarization (empty = use default)' required: false - summarization-max-iters: - description: 'Max iterations for summarization' - required: false - summarization-reasoning-effort: description: 'Reasoning effort for summarization (minimal|low|medium|high)' required: false @@ -308,7 +300,6 @@ runs: # Doc review signature DOC_ENABLED: ${{ inputs.doc-enabled }} DOC_MODEL: ${{ inputs.doc-model }} - DOC_MAX_ITERS: ${{ inputs.doc-max-iters }} DOC_REASONING_EFFORT: ${{ inputs.doc-reasoning-effort }} DOC_TEMPERATURE: ${{ inputs.doc-temperature }} @@ -323,7 +314,6 @@ runs: # Summarization signature SUMMARIZATION_ENABLED: ${{ inputs.summarization-enabled }} SUMMARIZATION_MODEL: ${{ inputs.summarization-model }} - SUMMARIZATION_MAX_ITERS: ${{ inputs.summarization-max-iters }} SUMMARIZATION_REASONING_EFFORT: ${{ inputs.summarization-reasoning-effort }} SUMMARIZATION_TEMPERATURE: ${{ inputs.summarization-temperature }} @@ -370,7 +360,6 @@ runs: # Doc review [ -n "$DOC_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_ENABLED" [ -n "$DOC_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MODEL" - [ -n "$DOC_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MAX_ITERS" [ -n "$DOC_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_REASONING_EFFORT" [ -n "$DOC_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_TEMPERATURE" @@ -385,7 +374,6 @@ runs: # Summarization [ -n "$SUMMARIZATION_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_ENABLED" [ -n "$SUMMARIZATION_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MODEL" - [ -n "$SUMMARIZATION_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MAX_ITERS" [ -n "$SUMMARIZATION_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_REASONING_EFFORT" [ -n "$SUMMARIZATION_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_TEMPERATURE" diff --git a/codespy.yaml b/codespy.yaml index 4b7dc42..6b53073 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -82,8 +82,22 @@ memory: # Reflection defaults — overridable per-signature via signatures..memory default_enabled: false # MEMORY_DEFAULT_ENABLED default_max_reflects: 0 # MEMORY_DEFAULT_MAX_REFLECTS (0 = reflect once at end_episode) - default_token_budget: 1024 # MEMORY_DEFAULT_TOKEN_BUDGET - default_max_trajectory_tokens: null # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS (null = full trajectory) + + # Three independent token budgets, from most to least cost-sensitive: + # + # 1. max_context_map_tokens — ceiling on the rendered ContextMap. This is the + # persisted artifact, and it is prepended to every agent iteration, so it is + # re-sent ~default_max_iters times per scope. 1024 holds ~12 items. + # 2. max_trajectory_tokens — cap on the trajectory fed to the Distiller. Tool-using + # agents can produce 100k+ token trajectories and TwoStepAdapter sends the value + # twice, so keep this to ~5-10% of the reflection model's context window. + # null = full trajectory (unbounded — not recommended for tool-using agents). + # 3. max_question_tokens — cap on the serialized agent inputs used as the reflection + # "question". Without it, every input field is sent in full (for code review that + # means the complete patch of every changed file). null = unbounded. + default_max_context_map_tokens: 1024 # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS + default_max_trajectory_tokens: 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS + default_max_question_tokens: 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS # LLM settings for the reflection modules. The Distiller summarizes a # trajectory; the Cartographer curates the context map. Both are compact @@ -170,8 +184,9 @@ signatures: memory: enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS - token_budget: null # SUPPLY_CHAIN_MEMORY_TOKEN_BUDGET + max_context_map_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS # Code Reviewer signature (bugs, security, removed defensive code, code smells) # Unified code review: bugs, security vulnerabilities, and code smells in a single agent pass per scope @@ -184,8 +199,9 @@ signatures: memory: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS - token_budget: null # CODE_REVIEW_MEMORY_TOKEN_BUDGET + max_context_map_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS # Documentation Reviewer signature (compares patches against extracted documentation) # Note: doc extraction is now deterministic (no LLM) — see doc_extractor.py @@ -197,8 +213,9 @@ signatures: memory: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # DOC_MEMORY_MAX_REFLECTS - token_budget: null # DOC_MEMORY_TOKEN_BUDGET + max_context_map_tokens: null # DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # DOC_MEMORY_MAX_QUESTION_TOKENS # Scope Identifier signature scope: @@ -210,8 +227,10 @@ signatures: memory: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS - token_budget: null # SCOPE_MEMORY_TOKEN_BUDGET + max_context_map_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS + # Unused: scope passes question_field="mr_title", so no inputs are serialized. + max_question_tokens: null # SCOPE_MEMORY_MAX_QUESTION_TOKENS # Summarizer signature summarization: @@ -222,8 +241,9 @@ signatures: memory: enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS - token_budget: null # SUMMARIZATION_MEMORY_TOKEN_BUDGET + max_context_map_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS max_trajectory_tokens: null # SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS # ============================================================================ # OUTPUT diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index cb6a558..c2b88f8 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -34,7 +34,7 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: All fields are included in full. If max_tokens is set, the joined result is head+tail bounded via _head_tail_text so both the instruction and any - trailing intent survive. See Hippocampus.max_input_tokens for guidance on + trailing intent survive. See Hippocampus.max_question_tokens for guidance on when and how to set a limit. """ parts: list[str] = [] @@ -90,36 +90,67 @@ def _format_step(i: int, entry: dict) -> str: return "\n".join(parts) +# Tokens set aside for the "... (N tokens omitted) ..." marker so the returned +# text honours max_tokens including the marker itself. +_MARKER_RESERVE = 16 + + +def _slice_tokens(text: str, n: int, from_end: bool = False) -> str: + """Return the first (or last) ``n`` tokens of ``text`` as a string. + + Used to split a line that is itself larger than the remaining budget, so + line-granularity bounding never has to discard a line wholesale. + """ + if n <= 0: + return "" + tokens = _ENCODING.encode(text) + kept = tokens[-n:] if from_end else tokens[:n] + return _ENCODING.decode(kept) + + def _head_tail_text(text: str, max_tokens: int, head_ratio: float = 0.6) -> str: """Keep the first head_ratio and last (1-head_ratio) of the token budget, dropping the middle with an omission marker. - Operates at line granularity; returns text unchanged if it fits. + Prefers line granularity, but falls back to token granularity for a line + that alone exceeds the remaining budget — without that fallback, a single + long line (e.g. the one-line repr of a pydantic input field) would blow the + whole budget and return almost nothing. Returns text unchanged if it fits. + + The omission marker is charged against ``max_tokens`` (see _MARKER_RESERVE), + so the result never exceeds the budget. """ if count_tokens(text) <= max_tokens: return text - head_budget = int(max_tokens * head_ratio) - tail_budget = max_tokens - head_budget + content_budget = max(max_tokens - _MARKER_RESERVE, 0) + head_budget = int(content_budget * head_ratio) + tail_budget = content_budget - head_budget lines = text.splitlines(keepends=True) - # Collect head lines + # Collect head lines, token-slicing the line that straddles the budget. head_lines: list[str] = [] head_tokens = 0 for line in lines: lt = count_tokens(line) if head_tokens + lt > head_budget: + head_lines.append(_slice_tokens(line, head_budget - head_tokens)) + head_tokens = head_budget break head_lines.append(line) head_tokens += lt - # Collect tail lines (from the end) + # Collect tail lines (from the end), same straddle handling. tail_lines: list[str] = [] tail_tokens = 0 for line in reversed(lines): lt = count_tokens(line) if tail_tokens + lt > tail_budget: + tail_lines.append( + _slice_tokens(line, tail_budget - tail_tokens, from_end=True) + ) + tail_tokens = tail_budget break tail_lines.append(line) tail_tokens += lt @@ -166,8 +197,9 @@ def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> s if count_tokens(full) <= max_tokens: return full - head_budget = int(max_tokens * 0.6) - tail_budget = max_tokens - head_budget + content_budget = max(max_tokens - _MARKER_RESERVE, 0) + head_budget = int(content_budget * 0.6) + tail_budget = content_budget - head_budget # Cap individual oversized step outputs before budgeting capped: list[str] = [] @@ -186,10 +218,10 @@ def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> s head_steps.append(s) head_tokens += t - # Greedily keep tail steps (from the end) + # Greedily keep tail steps (from the end), never reusing a head step tail_steps: list[str] = [] tail_tokens = 0 - for s in reversed(capped): + for s in reversed(capped[len(head_steps):]): t = count_tokens(s) if tail_tokens + t > tail_budget: break @@ -197,6 +229,14 @@ def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> s tail_tokens += t tail_steps.reverse() + # If no whole step fits in either half (every step is larger than its + # budget), fall back to bounding single steps so the budget is actually + # used instead of returning just the omission marker. + if not head_steps and capped: + head_steps = [_head_tail_text(capped[0], head_budget)] + if not tail_steps and len(capped) > len(head_steps): + tail_steps = [_head_tail_text(capped[-1], tail_budget)] + # Determine omitted range n_head = len(head_steps) n_tail = len(tail_steps) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 7ff1c4a..d6eeb1a 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -92,35 +92,55 @@ class Hippocampus(dspy.Module): - **Stage 2** (``end_episode``) — the joined episode is head+tail bounded again, so the combined result is guaranteed to fit the budget even if many calls are buffered. - With ``max_trajectory_tokens=None`` (default) both stages are no-ops and the Distiller + With ``max_trajectory_tokens=None`` both stages are no-ops and the Distiller receives the full trajectory. + + ## The three token budgets + + All three are measured with the ``o200k_base`` encoding: + + - ``max_context_map_tokens`` — the rendered ContextMap. This is the *persisted* + artifact and it is prepended to every predictor of the wrapped agent, so it is + re-sent on every agent iteration (~``max_iters`` times per run) plus once per + reflection call. The most cost-sensitive of the three. + - ``max_trajectory_tokens`` — the trajectory fed to the Distiller. + - ``max_question_tokens`` — the serialized inputs used as the reflection + "question" (only when ``question_field`` is unset). """ def __init__( self, module: dspy.Module, - token_budget: int = 1024, - max_trajectory_tokens: int | None = None, - max_input_tokens: int | None = None, + max_context_map_tokens: int = 1024, + max_trajectory_tokens: int | None = 8192, + max_question_tokens: int | None = 2048, max_reflects: int | None = None, question_field: str | None = None, ): """ Args: module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. - token_budget: Maximum tokens kept in the context map. + max_context_map_tokens: Hard ceiling on the rendered context map, + enforced by the Evictor after every reflection. At ~80 tokens per + item (the Cartographer's limit) 1024 holds ~12 items. Raise with + care: the map is re-sent on every agent iteration, so the cost is + multiplied by ``max_iters``. max_trajectory_tokens: Token budget for trajectories fed to the Distiller. - None (default) = full trajectory, Distiller does all compression — - recommended for most cases. When set, use ≤ ~50 % of the Distiller - model's context window (e.g. ~8192 for 128k, ~4096 for smaller). - Applied per call (stage 1) and again over the combined episode in - end_episode() (stage 2). Step-aware head+tail bounding (60 % head / - 40 % tail) preserves both setup and conclusions. - max_input_tokens: Token budget for serialized inputs used as the Distiller - "question" (fallback path when question_field is None). None (default) - = unbounded. Set ~1024 only when a large input field (e.g. an RLM - document dump) is serialized; keep it well below max_trajectory_tokens. - Ignored when question_field is set. + None = full trajectory, Distiller does all compression — only viable + for agents with short, predictable trajectories. Tool-using agents + can produce 100k+ token trajectories, and TwoStepAdapter sends the + value twice, so prefer ~5–10 % of the Distiller model's context + window (default 8192, i.e. ~5 % of 128k). Applied per call (stage 1) + and again over the combined episode in end_episode() (stage 2). + Step-aware head+tail bounding (60 % head / 40 % tail) preserves both + setup and conclusions. + max_question_tokens: Token budget for the serialized inputs used as the + reflection "question", on the fallback path when question_field is + None. None = unbounded, which is rarely safe: *every* input field is + serialized, so an agent taking a large field (a document dump, or a + diff of every changed file) sends all of it to both the Distiller and + the Cartographer. Ignored when question_field is set — prefer that + when a single field cleanly captures intent. max_reflects: Maximum number of forward() calls that also reflect online. None (default): no limit — reflect after every call (classic online learning). 0: never reflect online — pure buffering until end_episode(). @@ -129,7 +149,7 @@ def __init__( end_episode() is always available. question_field: Name of the input field carrying the task description. If set, only that field is used as the Distiller "question". - If None, all input fields are serialized (bounded by max_input_tokens). + If None, all input fields are serialized (bounded by max_question_tokens). Set this when one field cleanly captures user intent. """ super().__init__() @@ -152,9 +172,9 @@ def __init__( self.agent = module self.distill = Distiller() self.cartograph = Cartographer() - self.token_budget = token_budget + self.max_context_map_tokens = max_context_map_tokens self.max_trajectory_tokens = max_trajectory_tokens - self.max_input_tokens = max_input_tokens + self.max_question_tokens = max_question_tokens self.max_reflects = max_reflects self.question_field = question_field self.cmap = ContextMap() @@ -373,7 +393,7 @@ def reset_episode(self) -> None: def _make_question(self, inputs: dict) -> str: if self.question_field is not None: return str(inputs.get(self.question_field, "")) - return format_inputs(inputs, self.max_input_tokens) + return format_inputs(inputs, self.max_question_tokens) def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( @@ -398,7 +418,9 @@ def _distill(self, trajectory: str, question: str) -> None: cache_candidates=list(distilled.cache_candidates or []), current_map=self.cmap, question=question, - token_budget=self.token_budget, + # The Cartographer's input field keeps the generic name: it is prompt + # text, already scoped by its description, and pairs with current_tokens. + token_budget=self.max_context_map_tokens, current_tokens=count_tokens(self.cmap.render()), ) ops = list(edits.operations or []) @@ -408,7 +430,7 @@ def _distill(self, trajectory: str, question: str) -> None: for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 - self.cmap = evict(self.cmap, self.scores, self.token_budget) + self.cmap = evict(self.cmap, self.scores, self.max_context_map_tokens) live = self.cmap.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index eb96586..093bd24 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -234,10 +234,17 @@ async def aforward( if self._settings.get_memory_enabled("code_review"): mem = Hippocampus( agent, - token_budget=self._settings.get_memory_token_budget("code_review"), + max_context_map_tokens=( + self._settings.get_memory_max_context_map_tokens("code_review") + ), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "code_review" ), + # No question_field: the scope (with every patch) is the + # only input, so the serialized question must be bounded. + max_question_tokens=self._settings.get_memory_max_question_tokens( + "code_review" + ), max_reflects=self._settings.get_memory_max_reflects("code_review"), ) result = await mem.aforward( diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 87e51b2..121f0ab 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -166,10 +166,17 @@ async def aforward( if self._settings.get_memory_enabled("doc"): mem = Hippocampus( reviewer, - token_budget=self._settings.get_memory_token_budget("doc"), + max_context_map_tokens=( + self._settings.get_memory_max_context_map_tokens("doc") + ), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "doc" ), + # No question_field: patches + documentation are both + # large, so the serialized question must be bounded. + max_question_tokens=self._settings.get_memory_max_question_tokens( + "doc" + ), max_reflects=self._settings.get_memory_max_reflects("doc"), ) result = await mem.aforward( diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 5f3f990..5b067df 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -262,11 +262,15 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal if self._settings.get_memory_enabled("scope"): mem = Hippocampus( agent, - token_budget=self._settings.get_memory_token_budget("scope"), + max_context_map_tokens=( + self._settings.get_memory_max_context_map_tokens("scope") + ), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "scope" ), max_reflects=self._settings.get_memory_max_reflects("scope"), + # question_field makes max_question_tokens moot: the title + # alone is the question, so no inputs get serialized. question_field="mr_title", ) result = await mem.aforward( diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index eb553a8..72821e8 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -311,14 +311,21 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list if self._settings.get_memory_enabled("supply_chain"): mem = Hippocampus( supply_chain_agent, - token_budget=self._settings.get_memory_token_budget( - "supply_chain" + max_context_map_tokens=( + self._settings.get_memory_max_context_map_tokens( + "supply_chain" + ) ), max_trajectory_tokens=( self._settings.get_memory_max_trajectory_tokens( "supply_chain" ) ), + max_question_tokens=( + self._settings.get_memory_max_question_tokens( + "supply_chain" + ) + ), max_reflects=self._settings.get_memory_max_reflects( "supply_chain" ), diff --git a/src/codespy/config.py b/src/codespy/config.py index c3ec09f..7233a29 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -247,17 +247,25 @@ def get_memory_max_reflects(self, signature_name: str) -> int | None: else self.memory.default_max_reflects ) - def get_memory_token_budget(self, signature_name: str) -> int: - """Get token_budget for a signature's memory (signature-specific or default).""" + def get_memory_max_context_map_tokens(self, signature_name: str) -> int: + """Get max_context_map_tokens for a signature's memory (signature-specific or default). + + Bounds the rendered ContextMap — the persisted artifact that is prepended + to every predictor of the wrapped agent, and therefore re-sent on every + ReAct iteration. + """ config = self.get_signature_config(signature_name).memory return ( - config.token_budget - if config.token_budget is not None - else self.memory.default_token_budget + config.max_context_map_tokens + if config.max_context_map_tokens is not None + else self.memory.default_max_context_map_tokens ) def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: - """Get max_trajectory_tokens for a signature's memory (signature-specific or default).""" + """Get max_trajectory_tokens for a signature's memory (signature-specific or default). + + Bounds the agent trajectory fed to the Distiller. + """ config = self.get_signature_config(signature_name).memory return ( config.max_trajectory_tokens @@ -265,6 +273,19 @@ def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: else self.memory.default_max_trajectory_tokens ) + def get_memory_max_question_tokens(self, signature_name: str) -> int | None: + """Get max_question_tokens for a signature's memory (signature-specific or default). + + Bounds the serialized agent inputs used as the reflection "question". + Ignored when the caller passes an explicit ``question_field``. + """ + config = self.get_signature_config(signature_name).memory + return ( + config.max_question_tokens + if config.max_question_tokens is not None + else self.memory.default_max_question_tokens + ) + def log_signature_configs(self) -> None: """Log all signature and reflection module LLM configurations.""" logger.info("Signature configurations:") diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index bedb941..94bb65c 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -24,10 +24,11 @@ class MemorySignatureConfig(BaseModel): (see ``codespy.config_memory.MemoryConfig``). """ - enabled: bool | None = None # _MEMORY_ENABLED - max_reflects: int | None = None # _MEMORY_MAX_REFLECTS - token_budget: int | None = None # _MEMORY_TOKEN_BUDGET - max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS + enabled: bool | None = None # _MEMORY_ENABLED + max_reflects: int | None = None # _MEMORY_MAX_REFLECTS + max_context_map_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MAP_TOKENS + max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: int | None = None # _MEMORY_MAX_QUESTION_TOKENS class SignatureConfig(BaseModel): @@ -90,7 +91,7 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - ``CODE_REVIEW_MAX_ITERS`` -> signatures.code_review.max_iters - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - - ``SCOPE_MEMORY_TOKEN_BUDGET`` -> signatures.scope.memory.token_budget + - ``SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS`` -> signatures.scope.memory.max_context_map_tokens Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) are handled directly by pydantic-settings and should NOT be processed here. diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 08f9854..362156d 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -72,8 +72,27 @@ class MemoryConfig(BaseModel): # Reflection defaults — overridable per-signature default_enabled: bool = False # MEMORY_DEFAULT_ENABLED default_max_reflects: int = Field(default=0) # MEMORY_DEFAULT_MAX_REFLECTS - default_token_budget: int = Field(default=1024) # MEMORY_DEFAULT_TOKEN_BUDGET - default_max_trajectory_tokens: int | None = None # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS + + # Ceiling on the rendered ContextMap. This is the *persisted* artifact and it + # is prepended to every predictor of the wrapped agent, so it is re-sent on + # every ReAct iteration (~default_max_iters times per scope) plus once per + # reflection call. Easily the most cost-sensitive of the three budgets. + # 1024 holds ~12 items at the Cartographer's ~80-tokens-per-item limit. + # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS + default_max_context_map_tokens: int = Field(default=1024) + + # Head+tail cap on the agent trajectory fed to the Distiller. Without it a + # single tool-heavy scope can produce a 100k+ token trajectory; TwoStepAdapter + # then sends it twice. 8192 is ~5% of a 128k window and preserves both the + # orientation steps (60% head) and the conclusions (40% tail). + default_max_trajectory_tokens: int | None = 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS + + # Head+tail cap on the serialized agent inputs used as the Distiller/Cartographer + # "question". Only applies when the caller passes no question_field: otherwise + # every input field is serialized, which for code review means the full patch + # of every changed file. See Hippocampus.max_question_tokens. + default_max_question_tokens: int | None = 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS + # Per-module LLM overrides for the reflection pipeline. # Unset fields fall back to the top-level ``default_*`` settings. @@ -98,8 +117,9 @@ class MemoryConfig(BaseModel): "S3_ENDPOINT_URL": "s3_endpoint_url", "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", - "DEFAULT_TOKEN_BUDGET": "default_token_budget", + "DEFAULT_MAX_CONTEXT_MAP_TOKENS": "default_max_context_map_tokens", "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", + "DEFAULT_MAX_QUESTION_TOKENS": "default_max_question_tokens", } # The reflection modules, derived from the MemoryConfig fields that hold a @@ -130,9 +150,9 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: Maps flat env vars onto the nested ``memory`` config, e.g.:: - MEMORY_BACKEND=s3 -> memory.backend - MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled - MEMORY_DEFAULT_TOKEN_BUDGET=512 -> memory.default_token_budget + MEMORY_BACKEND=s3 -> memory.backend + MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled + MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=512 -> memory.default_max_context_map_tokens Reflection module overrides use a second level of nesting:: From 1b150c4a963567b29ff06100801e26f8425d3262 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 27 Jul 2026 22:44:18 +0200 Subject: [PATCH 20/79] cfg --- .gitignore | 1 + tests/__init__.py | 0 2 files changed, 1 insertion(+) delete mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore index 0d1448d..d17395f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ htmlcov/ .coverage .coverage.* .cache +.ruff_cache nosetests.xml coverage.xml *.cover diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From e8d29be3ca21a2221595b7ba8742920a2396a378 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 28 Jul 2026 00:47:18 +0200 Subject: [PATCH 21/79] cfg --- .env.example | 47 +++++-- codespy.yaml | 42 +++++- src/codespy/agents/cost_tracker.py | 123 +++++++++++++----- src/codespy/agents/dspy_config.py | 84 +++++++++++- .../agents/memory/hippocampus/context_map.py | 4 +- .../agents/memory/hippocampus/episode.py | 5 +- .../agents/memory/hippocampus/hippocampus.py | 36 +++-- .../hippocampus/modules/cartographer.py | 13 +- .../memory/hippocampus/modules/distiller.py | 20 ++- src/codespy/agents/reviewer/models.py | 5 +- .../agents/reviewer/modules/code_reviewer.py | 3 + .../agents/reviewer/modules/doc_reviewer.py | 1 + .../reviewer/modules/scope_identifier.py | 1 + .../reviewer/modules/supply_chain_auditor.py | 3 + src/codespy/config.py | 29 ++++- src/codespy/config_dspy.py | 3 + src/codespy/config_memory.py | 20 ++- src/codespy/tools/git/local_diff.py | 10 +- 18 files changed, 369 insertions(+), 80 deletions(-) diff --git a/.env.example b/.env.example index 225ba2e..251482c 100644 --- a/.env.example +++ b/.env.example @@ -130,6 +130,19 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Must be 1 while reasoning is enabled (providers reject other values) # DEFAULT_TEMPERATURE=1 +# Output token budget for a single completion (default: 64000). This is an OUTPUT +# ceiling, not a context window, and reasoning/thinking tokens are charged against +# it — so keep it well above the expected answer size while DEFAULT_REASONING_EFFORT +# is set. codespy always sends an explicit value: without one LiteLLM silently falls +# back to its own 4096 default and truncates responses mid-answer (DSPy warns +# "LM response was truncated ... max_tokens=None"). +# The value is clamped down to each model's real output ceiling before use, so a +# generous setting stays valid on smaller models; models LiteLLM doesn't know +# (Ollama, custom endpoints) receive it unchanged. +# NOTE: LiteLLM also reads DEFAULT_MAX_TOKENS for its own internal fallback, so +# setting this aligns both layers. +# DEFAULT_MAX_TOKENS=64000 + # Global LLM reliability settings # Number of retries for LLM API calls # LLM_RETRIES=3 @@ -169,18 +182,24 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # 0 = reflect once at end_episode # MEMORY_DEFAULT_MAX_REFLECTS=0 # -# Three independent token budgets, most to least cost-sensitive: +# Four independent token budgets, most to least cost-sensitive: # # 1. Ceiling on the rendered ContextMap. This is the persisted artifact, and it is # prepended to every agent iteration, so it is re-sent ~DEFAULT_MAX_ITERS times -# per scope. 1024 holds ~12 items. -# MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=1024 -# 2. Cap on the trajectory fed to the Distiller. Tool-using agents can produce +# per scope. Divided by MEMORY_DEFAULT_MAX_ITEM_TOKENS it gives the map's item +# capacity (3072 / 240 ~= 12 items). +# MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=3072 +# 2. Budget for a SINGLE context-map item, given to the Distiller and the +# Cartographer as a prompt input so no one item eats the whole map. Soft limit +# (expressed to the LLM, not enforced — truncating an item could corrupt an exact +# constant). Lower it for more, terser items; raise it for fewer, richer ones. +# MEMORY_DEFAULT_MAX_ITEM_TOKENS=240 +# 3. Cap on the trajectory fed to the Distiller. Tool-using agents can produce # 100k+ token trajectories and TwoStepAdapter sends the value twice, so keep this # to ~5-10% of the reflection model's context window. Unset = full trajectory # (unbounded — not recommended for tool-using agents). # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS=8192 -# 3. Cap on the serialized agent inputs used as the reflection "question". Without +# 4. Cap on the serialized agent inputs used as the reflection "question". Without # it, every input field is sent in full (for code review, the complete patch of # every changed file). Unset = unbounded. # MEMORY_DEFAULT_MAX_QUESTION_TOKENS=2048 @@ -193,11 +212,13 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # MEMORY_DISTILLER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_DISTILLER_REASONING_EFFORT=low # MEMORY_DISTILLER_TEMPERATURE=1 +# MEMORY_DISTILLER_MAX_TOKENS=64000 # MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_CARTOGRAPHER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_CARTOGRAPHER_REASONING_EFFORT=low # MEMORY_CARTOGRAPHER_TEMPERATURE=1 +# MEMORY_CARTOGRAPHER_MAX_TOKENS=64000 # ============================================================================= @@ -235,9 +256,11 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - MODEL (LiteLLM model string) # - REASONING_EFFORT (minimal|low|medium|high) - Provider reasoning budget # - TEMPERATURE (float) - Must be 1 while reasoning is enabled +# - MAX_TOKENS (integer) - Output token budget (unset -> DEFAULT_MAX_TOKENS) # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) # - MEMORY_MAX_CONTEXT_MAP_TOKENS (integer) - Ceiling on the persisted ContextMap +# - MEMORY_MAX_ITEM_TOKENS (integer) - Budget for a single context-map item # - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection # - MEMORY_MAX_QUESTION_TOKENS (integer) - Cap on serialized inputs used as the question @@ -247,10 +270,12 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 # CODE_REVIEW_REASONING_EFFORT=high # CODE_REVIEW_TEMPERATURE=1 +# CODE_REVIEW_MAX_TOKENS=64000 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# CODE_REVIEW_MEMORY_MAX_ITEM_TOKENS=240 # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -260,7 +285,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_SCAN_UNCHANGED=false # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SUPPLY_CHAIN_MEMORY_MAX_ITEM_TOKENS=240 # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -268,16 +294,19 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 -# DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# DOC_MEMORY_MAX_ITEM_TOKENS=240 # DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # DOC_MEMORY_MAX_QUESTION_TOKENS=2048 # SCOPE_ENABLED=true # SCOPE_MAX_ITERS=10 # SCOPE_REASONING_EFFORT=low +# SCOPE_MAX_TOKENS=64000 # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 -# SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS=1024 +# SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SCOPE_MEMORY_MAX_ITEM_TOKENS=240 # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # Unused: scope uses question_field="mr_title", so no inputs are serialized. # SCOPE_MEMORY_MAX_QUESTION_TOKENS= diff --git a/codespy.yaml b/codespy.yaml index 6b53073..7256c15 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -83,19 +83,26 @@ memory: default_enabled: false # MEMORY_DEFAULT_ENABLED default_max_reflects: 0 # MEMORY_DEFAULT_MAX_REFLECTS (0 = reflect once at end_episode) - # Three independent token budgets, from most to least cost-sensitive: + # Four independent token budgets, from most to least cost-sensitive: # # 1. max_context_map_tokens — ceiling on the rendered ContextMap. This is the # persisted artifact, and it is prepended to every agent iteration, so it is - # re-sent ~default_max_iters times per scope. 1024 holds ~12 items. - # 2. max_trajectory_tokens — cap on the trajectory fed to the Distiller. Tool-using + # re-sent ~default_max_iters times per scope. Divided by max_item_tokens it + # gives the map's item capacity (3072 / 240 ~= 12 items). + # 2. max_item_tokens — budget for a SINGLE context-map item, given to the Distiller + # and the Cartographer as a prompt input so no one item eats the whole map. + # Soft limit (expressed to the LLM, not enforced — truncating an item could + # corrupt an exact constant). Lower it for more, terser items; raise it for + # fewer, richer ones. + # 3. max_trajectory_tokens — cap on the trajectory fed to the Distiller. Tool-using # agents can produce 100k+ token trajectories and TwoStepAdapter sends the value # twice, so keep this to ~5-10% of the reflection model's context window. # null = full trajectory (unbounded — not recommended for tool-using agents). - # 3. max_question_tokens — cap on the serialized agent inputs used as the reflection + # 4. max_question_tokens — cap on the serialized agent inputs used as the reflection # "question". Without it, every input field is sent in full (for code review that # means the complete patch of every changed file). null = unbounded. - default_max_context_map_tokens: 1024 # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS + default_max_context_map_tokens: 3072 # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS + default_max_item_tokens: 240 # MEMORY_DEFAULT_MAX_ITEM_TOKENS default_max_trajectory_tokens: 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS default_max_question_tokens: 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS @@ -108,11 +115,13 @@ memory: extraction_model: null # MEMORY_DISTILLER_EXTRACTION_MODEL reasoning_effort: null # MEMORY_DISTILLER_REASONING_EFFORT temperature: null # MEMORY_DISTILLER_TEMPERATURE + max_tokens: null # MEMORY_DISTILLER_MAX_TOKENS cartographer: model: null # MEMORY_CARTOGRAPHER_MODEL extraction_model: null # MEMORY_CARTOGRAPHER_EXTRACTION_MODEL reasoning_effort: null # MEMORY_CARTOGRAPHER_REASONING_EFFORT temperature: null # MEMORY_CARTOGRAPHER_TEMPERATURE + max_tokens: null # MEMORY_CARTOGRAPHER_MAX_TOKENS # ============================================================================ @@ -120,7 +129,7 @@ memory: # ============================================================================ # Each signature config supports: enabled, max_iters, model, reasoning_effort, -# temperature. Set to null to inherit the corresponding default_* value. +# temperature, max_tokens. Set to null to inherit the corresponding default_* value. # # RECOMMENDED MODEL STRATEGY @@ -165,6 +174,17 @@ default_max_iters: 20 # DEFAULT_MAX_ITERS default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) default_temperature: 1 # DEFAULT_TEMPERATURE (must be 1 while reasoning is enabled) +# Output token budget for a single completion. This is an OUTPUT ceiling, not a +# context window, and reasoning/thinking tokens are charged against it — so it +# must be well above the expected answer size whenever reasoning_effort is set. +# Leaving it unset is not an option: LiteLLM then silently falls back to its own +# 4096 default, which truncates responses mid-answer (DSPy warns +# "LM response was truncated ... max_tokens=None"). +# The value is clamped down to each model's real output ceiling before use, so a +# generous setting stays valid on smaller models. Models LiteLLM doesn't know +# (Ollama, custom endpoints) receive it unchanged. +default_max_tokens: 64000 # DEFAULT_MAX_TOKENS + # Global LLM reliability settings llm_retries: 3 # LLM_RETRIES (number of retries for LLM API calls) @@ -178,6 +198,7 @@ signatures: model: null # SUPPLY_CHAIN_MODEL (Haiku 4.5) reasoning_effort: null # SUPPLY_CHAIN_REASONING_EFFORT temperature: null # SUPPLY_CHAIN_TEMPERATURE + max_tokens: null # SUPPLY_CHAIN_MAX_TOKENS scan_unchanged: false # SUPPLY_CHAIN_SCAN_UNCHANGED # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests # When false (default): only scans artifacts/manifests that were modified in the MR @@ -185,6 +206,7 @@ signatures: enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS @@ -196,10 +218,12 @@ signatures: model: null # CODE_REVIEW_MODEL reasoning_effort: null # CODE_REVIEW_REASONING_EFFORT temperature: null # CODE_REVIEW_TEMPERATURE + max_tokens: null # CODE_REVIEW_MAX_TOKENS memory: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS max_context_map_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: null # CODE_REVIEW_MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS @@ -210,10 +234,12 @@ signatures: model: null # DOC_MODEL reasoning_effort: null # DOC_REASONING_EFFORT temperature: null # DOC_TEMPERATURE + max_tokens: null # DOC_MAX_TOKENS memory: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # DOC_MEMORY_MAX_REFLECTS max_context_map_tokens: null # DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: null # DOC_MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # DOC_MEMORY_MAX_QUESTION_TOKENS @@ -224,10 +250,12 @@ signatures: model: null # SCOPE_MODEL reasoning_effort: null # SCOPE_REASONING_EFFORT temperature: null # SCOPE_TEMPERATURE + max_tokens: null # SCOPE_MAX_TOKENS memory: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: null # SCOPE_MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS # Unused: scope passes question_field="mr_title", so no inputs are serialized. max_question_tokens: null # SCOPE_MEMORY_MAX_QUESTION_TOKENS @@ -238,10 +266,12 @@ signatures: model: null # SUMMARIZATION_MODEL (falls back to default_model) reasoning_effort: null # SUMMARIZATION_REASONING_EFFORT temperature: null # SUMMARIZATION_TEMPERATURE + max_tokens: null # SUMMARIZATION_MAX_TOKENS memory: enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: null # SUMMARIZATION_MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: null # SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index 60a797a..9a1ad1c 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -4,10 +4,13 @@ even during parallel execution with dspy.Parallel. """ +import sys import threading import time +from contextlib import AbstractContextManager from dataclasses import dataclass -from typing import Optional +from types import TracebackType +from typing import Any, Optional import dspy # type: ignore[import-untyped] @@ -162,9 +165,36 @@ def _get_history_uuids() -> set[str]: return {entry.get("uuid", "") for entry in entries if entry.get("uuid")} +def _as_number(value: object) -> float: + """Coerce a history field to a number, yielding 0.0 for anything unusable. + + History entries are raw provider/LiteLLM payloads, so ``cost`` and the + ``usage`` counters are only *conventionally* numeric. Coercing here keeps a + provider-shape surprise from turning accounting into an exception. + + Args: + value: A value read from an LM history entry. + + Returns: + The value as a float, or 0.0 if it is missing or not numeric. + """ + if isinstance(value, bool) or value is None: + return 0.0 + if isinstance(value, (int, float)): + return float(value) + try: # Some providers report numbers as strings. + return float(value) # type: ignore[arg-type] + except (TypeError, ValueError): + return 0.0 + + def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) -> tuple[float, int, int]: """Calculate costs from history entries, excluding specific UUIDs. - + + Every field is read defensively: cost accounting is observability, so a + malformed entry degrades that entry to zero rather than failing the review + that produced it. + Args: entries: List of history entries exclude_uuids: Set of UUIDs to exclude from calculation @@ -173,27 +203,26 @@ def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) Tuple of (total_cost, total_tokens, call_count) """ total_cost = 0.0 - total_tokens = 0 + total_tokens = 0.0 call_count = 0 for entry in entries: + if not isinstance(entry, dict): + continue + entry_uuid = entry.get("uuid", "") if entry_uuid and entry_uuid not in exclude_uuids: - # Get cost - cost = entry.get("cost") - if cost is not None: - total_cost += cost - + total_cost += _as_number(entry.get("cost")) + # Get tokens from usage - usage = entry.get("usage", {}) - if usage: - prompt_tokens = usage.get("prompt_tokens", 0) or 0 - completion_tokens = usage.get("completion_tokens", 0) or 0 - total_tokens += prompt_tokens + completion_tokens - + usage = entry.get("usage") + if isinstance(usage, dict): + total_tokens += _as_number(usage.get("prompt_tokens")) + total_tokens += _as_number(usage.get("completion_tokens")) + call_count += 1 - return total_cost, total_tokens, call_count + return total_cost, int(total_tokens), call_count class SignatureContext: @@ -224,10 +253,18 @@ def __init__(self, signature_name: str, tracker: "CostTracker") -> None: self.signature_name = signature_name self.tracker = tracker self._before_uuids: set[str] = set() - self._lm_context = None + # Annotated so the None default doesn't narrow the attribute to + # ``None``, which would hide the enter/exit calls from type checking. + self._lm_context: AbstractContextManager[Any] | None = None def __enter__(self) -> "SignatureContext": - """Enter the context, applying the LM and capturing history state.""" + """Enter the context, applying the LM and capturing history state. + + If the bookkeeping that follows the LM swap fails, the swap is rolled + back before propagating: Python does not call ``__exit__`` when + ``__enter__`` raises, so without this the overridden LM would stay + installed for the rest of the thread. + """ # Imported here to avoid a circular import at module load time # (dspy_config imports codespy.config, which must not import agents). from codespy.agents.dspy_config import lm_context @@ -236,28 +273,52 @@ def __enter__(self) -> "SignatureContext": # to the LM that will actually serve the enclosed calls. self._lm_context = lm_context(self.signature_name) self._lm_context.__enter__() - self._before_uuids = _get_history_uuids() - self.tracker.start_signature(self.signature_name) + try: + self._before_uuids = _get_history_uuids() + self.tracker.start_signature(self.signature_name) + except BaseException: + self._lm_context.__exit__(*sys.exc_info()) + self._lm_context = None + raise return self - def __exit__(self, exc_type, exc_val, exc_tb) -> None: - """Exit the context, calculating costs from new history entries.""" - # Read history before leaving the LM context, so dspy.settings.lm still - # points at the LM whose history we need. - entries = _get_history_entries() - cost, tokens, call_count = _calculate_costs_from_entries(entries, self._before_uuids) - self.tracker.end_signature(self.signature_name, cost, tokens, call_count) - - if self._lm_context is not None: - self._lm_context.__exit__(exc_type, exc_val, exc_tb) - self._lm_context = None + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the context, calculating costs from new history entries. + + The LM context is released in a ``finally``: a leaked + ``dspy.context`` does not raise, it silently leaves the overridden LM + installed for the remainder of the thread, so every later predictor + would run on the wrong model and be attributed to the wrong signature. + Guaranteeing the exit keeps an accounting failure loud and local + instead of quiet and global. + """ + try: + # Read history before leaving the LM context, so dspy.settings.lm + # still points at the LM whose history we need. + entries = _get_history_entries() + cost, tokens, call_count = _calculate_costs_from_entries(entries, self._before_uuids) + self.tracker.end_signature(self.signature_name, cost, tokens, call_count) + finally: + if self._lm_context is not None: + self._lm_context.__exit__(exc_type, exc_val, exc_tb) + self._lm_context = None async def __aenter__(self) -> "SignatureContext": """Async enter the context.""" return self.__enter__() - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: """Async exit the context.""" self.__exit__(exc_type, exc_val, exc_tb) diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 312491b..92bcbbb 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -13,15 +13,81 @@ logger = logging.getLogger(__name__) +def _resolve_max_tokens(model: str, max_tokens: int) -> int: + """Clamp a configured output budget to the model's real output ceiling. + + ``max_tokens`` is an *output* budget, not a context window, and providers + reject a request that asks for more than the model can emit (Anthropic + returns a 400). Clamping lets a single generous default be configured + globally and still resolve to a valid value per model. + + Models LiteLLM doesn't know about (Ollama, custom endpoints) have no + published ceiling, so the configured value is passed through unchanged. + + Args: + model: The LiteLLM model identifier. + max_tokens: The configured output token budget. + + Returns: + The budget, reduced to the model's ceiling when one is known. + """ + try: + ceiling = litellm.get_max_tokens(model) + except Exception: # Unmapped model — no published ceiling to clamp to. + return max_tokens + if not ceiling: + return max_tokens + return min(max_tokens, ceiling) + + +def _supports_reasoning_effort(model: str) -> bool | None: + """Whether LiteLLM maps ``reasoning_effort`` onto this model's provider. + + Three outcomes, because "unknown" must not be conflated with + "unsupported": + + - ``True`` — LiteLLM knows the model and maps the parameter. + - ``False`` — LiteLLM knows the model and does *not* map it. Sending it + anyway raises ``UnsupportedParamsError`` on every request, because + ``litellm.drop_params`` defaults to False. + - ``None`` — LiteLLM has no parameter list for the model (Ollama, a + proxy, a custom endpoint). There is no published support to check, so + the caller should pass the value through and let the provider decide. + + Args: + model: The LiteLLM model identifier. + + Returns: + True / False when known, None when the model is unmapped. + """ + try: + params = litellm.get_supported_openai_params(model=model) + except Exception: # Unrecognised provider — treat as unmapped. + return None + if params is None: + return None + return "reasoning_effort" in params + + def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: """Build a ``dspy.LM`` for resolved LLM settings. The single place LMs are constructed, so the cross-cutting concerns - (timeout, retries, provider-side prompt caching) are applied uniformly. + (timeout, retries, output budget, provider-side prompt caching) are applied + uniformly. ``reasoning_effort`` is forwarded to LiteLLM through ``dspy.LM``'s ``**kwargs``; LiteLLM maps it onto each provider's native parameter - (Anthropic thinking budget, OpenAI reasoning effort, ...). + (Anthropic ``thinking.budget_tokens``, OpenAI ``reasoning_effort``, + Ollama ``think``, ...). It is omitted for models LiteLLM knows do not + support it: ``litellm.drop_params`` defaults to False, so sending it to + such a model raises on *every* request, and callers log-and-continue on + LLM errors — which would yield an empty review that still exits 0. + + ``max_tokens`` must be passed explicitly: omitting it makes LiteLLM fall + back to its own 4096 default, which silently truncates responses (DSPy then + warns about ``max_tokens=None``). Reasoning tokens are charged against this + budget, so it is clamped to — not capped below — the model's ceiling. Args: settings: Application settings (timeout / retries / prompt caching). @@ -33,10 +99,21 @@ def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: lm_kwargs: dict = { "model": config.model, "temperature": config.temperature, - "reasoning_effort": config.reasoning_effort, + "max_tokens": _resolve_max_tokens(config.model, config.max_tokens), "timeout": settings.llm_timeout, "num_retries": settings.llm_retries, } + # Only omit the effort when LiteLLM positively reports it unsupported; + # unmapped models (None) still get it, so reasoning is never silently + # disabled for a model that actually honours it. + if _supports_reasoning_effort(config.model) is False: + logger.warning( + f"Model {config.model} does not support reasoning_effort - ignoring " + f"reasoning_effort={config.reasoning_effort}. Sending it would fail " + f"every request to this model." + ) + else: + lm_kwargs["reasoning_effort"] = config.reasoning_effort # Cache system prompts on the provider's servers (Anthropic, OpenAI, Bedrock...) if settings.enable_prompt_caching: lm_kwargs["cache_control_injection_points"] = [ @@ -123,6 +200,7 @@ def configure_dspy(settings: Settings) -> None: logger.info( f"Configured DSPy with model: {model} " f"(TwoStepAdapter with extraction_model={extraction_model}, " + f"max_tokens={_resolve_max_tokens(defaults.model, defaults.max_tokens)}, " f"timeout={settings.llm_timeout}s, retries={settings.llm_retries}, " f"provider prompt caching {prompt_cache_status})" ) diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py index c0cb8d1..e62f65b 100644 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ b/src/codespy/agents/memory/hippocampus/context_map.py @@ -54,7 +54,9 @@ class Item(BaseModel): class CacheCandidate(BaseModel): section: SectionName - value: str = Field(description="Compact candidate cache item (<= ~80 tokens).") + value: str = Field( + description="Compact candidate cache item, within the max_item_tokens budget." + ) transferability: str = Field(description="Kinds of future questions this would help.") rationale: str = Field(description="Why this is shared understanding, not a one-off fact.") diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 4392924..efec582 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from pydantic import BaseModel, Field @@ -34,7 +34,8 @@ class Episode(BaseModel): module: str = Field(description="Wrapped dspy.Module class name") context_map: ContextMap = Field(description="Consolidated context map snapshot") timestamp: datetime = Field( - default_factory=datetime.utcnow, description="UTC time the episode was recorded" + default_factory=lambda: datetime.now(UTC), + description="UTC time the episode was recorded", ) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index d6eeb1a..4b5f07b 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -3,7 +3,7 @@ import asyncio import copy import uuid -from datetime import datetime +from datetime import UTC, datetime import dspy @@ -95,14 +95,19 @@ class Hippocampus(dspy.Module): With ``max_trajectory_tokens=None`` both stages are no-ops and the Distiller receives the full trajectory. - ## The three token budgets + ## The four token budgets - All three are measured with the ``o200k_base`` encoding: + All four are measured with the ``o200k_base`` encoding: - ``max_context_map_tokens`` — the rendered ContextMap. This is the *persisted* artifact and it is prepended to every predictor of the wrapped agent, so it is re-sent on every agent iteration (~``max_iters`` times per run) plus once per - reflection call. The most cost-sensitive of the three. + reflection call. The most cost-sensitive of the four. + - ``max_item_tokens`` — a *single* context-map item. Unlike the other three this + is a **soft** budget: it is passed to the Distiller and the Cartographer as a + prompt input so they keep each item compact, but it is not enforced in code + (truncating an item could corrupt an exact constant). Roughly, + ``max_context_map_tokens / max_item_tokens`` is the map's item capacity. - ``max_trajectory_tokens`` — the trajectory fed to the Distiller. - ``max_question_tokens`` — the serialized inputs used as the reflection "question" (only when ``question_field`` is unset). @@ -111,7 +116,8 @@ class Hippocampus(dspy.Module): def __init__( self, module: dspy.Module, - max_context_map_tokens: int = 1024, + max_context_map_tokens: int = 3072, + max_item_tokens: int = 240, max_trajectory_tokens: int | None = 8192, max_question_tokens: int | None = 2048, max_reflects: int | None = None, @@ -121,10 +127,17 @@ def __init__( Args: module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. max_context_map_tokens: Hard ceiling on the rendered context map, - enforced by the Evictor after every reflection. At ~80 tokens per - item (the Cartographer's limit) 1024 holds ~12 items. Raise with - care: the map is re-sent on every agent iteration, so the cost is - multiplied by ``max_iters``. + enforced by the Evictor after every reflection. Divided by + ``max_item_tokens`` it gives the map's approximate item capacity + (3072 / 240 ~= 12 items). Raise with care: the map is re-sent on + every agent iteration, so the cost is multiplied by ``max_iters``. + max_item_tokens: Token budget for a *single* context-map item, passed to + the Distiller and the Cartographer as a prompt input so they keep + each item compact rather than spending the whole map budget on one + verbose entry. Soft limit — expressed to the LLM, not enforced in + code, since truncating an item could corrupt an exact constant it + holds. Lower it to fit more, terser items in the same map budget; + raise it to allow richer items. max_trajectory_tokens: Token budget for trajectories fed to the Distiller. None = full trajectory, Distiller does all compression — only viable for agents with short, predictable trajectories. Tool-using agents @@ -173,6 +186,7 @@ def __init__( self.distill = Distiller() self.cartograph = Cartographer() self.max_context_map_tokens = max_context_map_tokens + self.max_item_tokens = max_item_tokens self.max_trajectory_tokens = max_trajectory_tokens self.max_question_tokens = max_question_tokens self.max_reflects = max_reflects @@ -259,7 +273,7 @@ def _finalize_episode(self) -> None: task=self._task_name, module=self._module_name, context_map=self.cmap.model_copy(deep=True), - timestamp=datetime.utcnow(), + timestamp=datetime.now(UTC), ) self._episode_trajectories.clear() self._episode_question = None @@ -400,6 +414,7 @@ def _distill(self, trajectory: str, question: str) -> None: trajectory=trajectory, context_map=self.cmap, question=question, + max_item_tokens=self.max_item_tokens, ) known = self.cmap.ids() @@ -422,6 +437,7 @@ def _distill(self, trajectory: str, question: str) -> None: # text, already scoped by its description, and pairs with current_tokens. token_budget=self.max_context_map_tokens, current_tokens=count_tokens(self.cmap.render()), + max_item_tokens=self.max_item_tokens, ) ops = list(edits.operations or []) diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index dc6d921..fcb2825 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -32,9 +32,9 @@ class CartographerSig(dspy.Signature): - Rewrite items when a more compact or more useful version exists. Prefer REPLACE over ADD when possible. - Add new items only when they represent transferable understanding. - - Each item must be short and budget-efficient — max ~80 tokens per - item. If a candidate exceeds this, rewrite it more compactly or - split it. + - Each item must be short and budget-efficient — stay within the + `max_item_tokens` budget given as an input. If a candidate exceeds + it, rewrite it more compactly or split it. - If nothing new is worth keeping, return an empty operations list. The litmus test: For each item, ask "Would a future agent asking a @@ -110,6 +110,10 @@ class CartographerSig(dspy.Signature): question: str = dspy.InputField(desc="Question the agent was answering.") token_budget: int = dspy.InputField(desc="Hard token budget for the context map.") current_tokens: int = dspy.InputField(desc="Current token count of the context map.") + max_item_tokens: int = dspy.InputField( + desc="Token budget for a SINGLE context-map item. Every ADD/REPLACE content " + "must stay within it." + ) justification: str = dspy.OutputField( desc="Brief explanation of why these edits improve the shared understanding " @@ -139,7 +143,7 @@ def __init__(self): self.predict = dspy.ChainOfThought(CartographerSig) def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, - token_budget, current_tokens): + token_budget, current_tokens, max_item_tokens): # See Distiller.forward: SignatureContext applies memory.cartographer's # LLM settings and gives this module its own cost line. Entered here # because DSPy's context is thread-scoped and reflection runs in a worker. @@ -154,5 +158,6 @@ def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, question=question, token_budget=token_budget, current_tokens=current_tokens, + max_item_tokens=max_item_tokens, ) diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index f051aa2..9df9f1b 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -109,6 +109,10 @@ class DistillerSig(dspy.Signature): trajectory: str = dspy.InputField(desc="The agent's full execution trajectory.") context_map: ContextMap = dspy.InputField(desc="Current context map (with item IDs).") question: str = dspy.InputField(desc="The question the agent was answering.") + max_item_tokens: int = dspy.InputField( + desc="Token budget for a SINGLE context-map item. Keep every candidate within " + "it; if one exceeds it, rewrite it more compactly or split it." + ) diagnosis: str = dspy.OutputField( desc="Brief (3-5 sentence) analysis of orientation vs. question-specific work, " @@ -120,7 +124,8 @@ class DistillerSig(dspy.Signature): "Keys must match existing item ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( - desc="Candidate items to add. Each <= ~80 tokens; structural/transferable only. " + desc="Candidate items to add. Each within the max_item_tokens budget; " + "structural/transferable only. " "Each candidate's `section` must be one of the five section names above." ) @@ -141,7 +146,13 @@ def __init__(self): super().__init__() self.predict = dspy.ChainOfThought(DistillerSig) - def forward(self, trajectory: str, context_map: ContextMap, question: str): + def forward( + self, + trajectory: str, + context_map: ContextMap, + question: str, + max_item_tokens: int, + ): # SignatureContext applies memory.distiller's model/temperature/reasoning # effort and attributes the cost to this module rather than to whichever # agent triggered the reflection. It must be entered here, not by the @@ -151,6 +162,9 @@ def forward(self, trajectory: str, context_map: ContextMap, question: str): with SignatureContext(self.SIGNATURE, get_cost_tracker()): return self.predict( - trajectory=trajectory, context_map=context_map, question=question + trajectory=trajectory, + context_map=context_map, + question=question, + max_item_tokens=max_item_tokens, ) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index bd8a839..4c6fa0f 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -1,6 +1,6 @@ """Data models for code review results.""" -from datetime import datetime +from datetime import UTC, datetime from enum import Enum from pathlib import Path @@ -149,7 +149,8 @@ class ReviewResult(BaseModel): mr_url: str = Field(description="MR URL") repo: str = Field(description="Repository name (owner/repo)") reviewed_at: datetime = Field( - default_factory=datetime.utcnow, description="Review timestamp" + default_factory=lambda: datetime.now(UTC), + description="Review timestamp", ) model_used: str = Field(description="LLM model used for review") issues: list[Issue] = Field( diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 093bd24..34e7edd 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -237,6 +237,9 @@ async def aforward( max_context_map_tokens=( self._settings.get_memory_max_context_map_tokens("code_review") ), + max_item_tokens=self._settings.get_memory_max_item_tokens( + "code_review" + ), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "code_review" ), diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 121f0ab..d67be23 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -169,6 +169,7 @@ async def aforward( max_context_map_tokens=( self._settings.get_memory_max_context_map_tokens("doc") ), + max_item_tokens=self._settings.get_memory_max_item_tokens("doc"), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "doc" ), diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 5b067df..cffa56b 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -265,6 +265,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal max_context_map_tokens=( self._settings.get_memory_max_context_map_tokens("scope") ), + max_item_tokens=self._settings.get_memory_max_item_tokens("scope"), max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( "scope" ), diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 72821e8..638a782 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -316,6 +316,9 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list "supply_chain" ) ), + max_item_tokens=( + self._settings.get_memory_max_item_tokens("supply_chain") + ), max_trajectory_tokens=( self._settings.get_memory_max_trajectory_tokens( "supply_chain" diff --git a/src/codespy/config.py b/src/codespy/config.py index 7233a29..c0de321 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -122,6 +122,13 @@ class Settings(BaseSettings): default_reasoning_effort: ReasoningEffort = "medium" # Providers require temperature=1 when reasoning is enabled. default_temperature: float = 1.0 + # Output token budget per completion. Must be set explicitly: when it is + # omitted LiteLLM silently falls back to its own 4096 default, which + # truncates reasoning models (thinking tokens are charged against this + # budget) and long structured outputs. 64000 matches the output ceiling of + # the Claude 4.x tier and satisfies dspy.LM's >=16000 guard for OpenAI + # reasoning models; new_lm() clamps it down to each model's real ceiling. + default_max_tokens: int = 64000 # Global LLM reliability settings @@ -217,6 +224,7 @@ def get_llm_config(self, name: str) -> LLMSettings: if config.temperature is not None else self.default_temperature ), + max_tokens=config.max_tokens or self.default_max_tokens, ) @@ -261,6 +269,21 @@ def get_memory_max_context_map_tokens(self, signature_name: str) -> int: else self.memory.default_max_context_map_tokens ) + def get_memory_max_item_tokens(self, signature_name: str) -> int: + """Get max_item_tokens for a signature's memory (signature-specific or default). + + Bounds a *single* context-map item. Handed to the Distiller and the + Cartographer as a prompt input so they keep each item compact instead of + spending the whole map budget on one verbose entry. Soft limit — the hard, + map-wide ceiling is ``get_memory_max_context_map_tokens``. + """ + config = self.get_signature_config(signature_name).memory + return ( + config.max_item_tokens + if config.max_item_tokens is not None + else self.memory.default_max_item_tokens + ) + def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: """Get max_trajectory_tokens for a signature's memory (signature-specific or default). @@ -295,14 +318,16 @@ def log_signature_configs(self) -> None: logger.info( f" {sig_name}: {status}, model={llm.model}, " f"max_iters={self.get_max_iters(sig_name)}, " - f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}" + f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}, " + f"max_tokens={llm.max_tokens}" ) for module in REFLECTION_MODULES: llm = self.get_llm_config(module) logger.info( f" {module}: model={llm.model}, " f"extraction_model={llm.extraction_model}, " - f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}" + f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}, " + f"max_tokens={llm.max_tokens}" ) diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index 94bb65c..f5cc7b0 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -27,6 +27,7 @@ class MemorySignatureConfig(BaseModel): enabled: bool | None = None # _MEMORY_ENABLED max_reflects: int | None = None # _MEMORY_MAX_REFLECTS max_context_map_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MAP_TOKENS + max_item_tokens: int | None = None # _MEMORY_MAX_ITEM_TOKENS max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: int | None = None # _MEMORY_MAX_QUESTION_TOKENS @@ -39,6 +40,7 @@ class SignatureConfig(BaseModel): model: str | None = None reasoning_effort: ReasoningEffort | None = None # Provider reasoning budget temperature: float | None = None # Must be 1 when reasoning is enabled + max_tokens: int | None = None # Output token budget (reasoning tokens included) scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests memory: MemorySignatureConfig = Field(default_factory=MemorySignatureConfig) @@ -92,6 +94,7 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - ``SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS`` -> signatures.scope.memory.max_context_map_tokens + - ``SCOPE_MEMORY_MAX_ITEM_TOKENS`` -> signatures.scope.memory.max_item_tokens Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) are handled directly by pydantic-settings and should NOT be processed here. diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 362156d..6fc1f5c 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -34,6 +34,7 @@ class ReflectionModuleConfig(BaseModel): extraction_model: str | None = None # MEMORY__EXTRACTION_MODEL reasoning_effort: ReasoningEffort | None = None # MEMORY__REASONING_EFFORT temperature: float | None = None # MEMORY__TEMPERATURE + max_tokens: int | None = None # MEMORY__MAX_TOKENS class LLMSettings(BaseModel): @@ -49,6 +50,10 @@ class LLMSettings(BaseModel): extraction_model: str reasoning_effort: ReasoningEffort temperature: float + # Output token budget for a single completion. Reasoning/thinking tokens are + # charged against it, so it must comfortably exceed the expected answer size. + # ``new_lm`` clamps this to the model's real output ceiling before use. + max_tokens: int @@ -77,9 +82,19 @@ class MemoryConfig(BaseModel): # is prepended to every predictor of the wrapped agent, so it is re-sent on # every ReAct iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. - # 1024 holds ~12 items at the Cartographer's ~80-tokens-per-item limit. + # Approximate item capacity is default_max_context_map_tokens divided by + # default_max_item_tokens (3072 / 240 ~= 12 items). # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS - default_max_context_map_tokens: int = Field(default=1024) + default_max_context_map_tokens: int = Field(default=3072) + + # Per-item ceiling handed to the Distiller/Cartographer as a prompt input, so + # they keep each context-map item compact instead of spending the whole map + # budget on one verbose entry. Soft limit: it is expressed to the LLM rather + # than enforced in code (truncating an item could corrupt an exact constant). + # The hard, map-wide limit is default_max_context_map_tokens, enforced by the + # Evictor. MEMORY_DEFAULT_MAX_ITEM_TOKENS + default_max_item_tokens: int = Field(default=240) + # Head+tail cap on the agent trajectory fed to the Distiller. Without it a # single tool-heavy scope can produce a 100k+ token trajectory; TwoStepAdapter @@ -118,6 +133,7 @@ class MemoryConfig(BaseModel): "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", "DEFAULT_MAX_CONTEXT_MAP_TOKENS": "default_max_context_map_tokens", + "DEFAULT_MAX_ITEM_TOKENS": "default_max_item_tokens", "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", "DEFAULT_MAX_QUESTION_TOKENS": "default_max_question_tokens", } diff --git a/src/codespy/tools/git/local_diff.py b/src/codespy/tools/git/local_diff.py index 2eae042..f8acfb1 100644 --- a/src/codespy/tools/git/local_diff.py +++ b/src/codespy/tools/git/local_diff.py @@ -2,7 +2,7 @@ import logging import subprocess -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from codespy.tools.git.models import ChangedFile, FileStatus, GitPlatform, MergeRequest @@ -163,8 +163,8 @@ def build_mr_from_diff( head_branch=head_branch, base_sha=base_sha, head_sha=head_sha, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), repo_owner=owner, repo_name=repo_name, host=host, @@ -217,8 +217,8 @@ def build_mr_from_diff( head_branch=head_branch, base_sha=base_sha, head_sha=head_sha, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), repo_owner=owner, repo_name=repo_name, host=host, From 0bd88ab7d41d6453cff6d85bda44e53c1ffd9842 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 28 Jul 2026 01:00:14 +0200 Subject: [PATCH 22/79] cfg --- .../agents/memory/hippocampus/hippocampus.py | 14 ++++++++++++-- .../agents/reviewer/modules/code_reviewer.py | 1 + .../agents/reviewer/modules/doc_reviewer.py | 3 +++ .../agents/reviewer/modules/scope_identifier.py | 1 + .../reviewer/modules/supply_chain_auditor.py | 1 + 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 4b5f07b..0433019 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -122,6 +122,7 @@ def __init__( max_question_tokens: int | None = 2048, max_reflects: int | None = None, question_field: str | None = None, + task_name: str | None = None, ): """ Args: @@ -164,6 +165,14 @@ def __init__( If set, only that field is used as the Distiller "question". If None, all input fields are serialized (bounded by max_question_tokens). Set this when one field cleanly captures user intent. + task_name: Identity recorded in ``Episode.task`` and used in the episode + filename. Pass the signature's snake_case name (``"doc"``, + ``"code_review"``, …) — the same key that drives config, LM + selection and cost attribution — so the episode path lines up with + the rest of the system. Inference is a last resort: only + ``dspy.ReAct``-style modules expose ``.signature``, + ``dspy.ChainOfThought`` does not, so the fallback would yield a + meaningless (and collision-prone) ``"ChainOfThought"``. """ super().__init__() @@ -203,8 +212,9 @@ def __init__( # Question derived from the first buffered call; used as Distiller input. self._episode_question: str | None = None - # Identity of the wrapped module/signature for Episode metadata. - self._task_name: str = ( + # Identity of the wrapped module/signature for Episode metadata. An explicit + # task_name wins: inference only works for modules exposing .signature. + self._task_name: str = task_name or ( top_sig.__name__ if top_sig is not None else type(module).__name__ ) self._module_name: str = type(module).__name__ diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 34e7edd..a4a4cd5 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -249,6 +249,7 @@ async def aforward( "code_review" ), max_reflects=self._settings.get_memory_max_reflects("code_review"), + task_name="code_review", ) result = await mem.aforward( scope=scoped, diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index d67be23..6d12c9d 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -179,6 +179,9 @@ async def aforward( "doc" ), max_reflects=self._settings.get_memory_max_reflects("doc"), + # ChainOfThought exposes no .signature, so the episode + # identity must be given explicitly. + task_name="doc", ) result = await mem.aforward( patches=patches, diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index cffa56b..0dfa17a 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -273,6 +273,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # question_field makes max_question_tokens moot: the title # alone is the question, so no inputs get serialized. question_field="mr_title", + task_name="scope", ) result = await mem.aforward( changed_files=changed_file_paths, diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 638a782..fa15f48 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -332,6 +332,7 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list max_reflects=self._settings.get_memory_max_reflects( "supply_chain" ), + task_name="supply_chain", ) result = await mem.aforward( manifest_path=manifest_path, From 51806fe1c40ee03964bddc6a054763bcec94176f Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 28 Jul 2026 02:11:19 +0200 Subject: [PATCH 23/79] cfg --- .env.example | 14 +-- codespy.yaml | 16 ++-- .../agents/memory/hippocampus/__init__.py | 2 + .../agents/memory/hippocampus/budget.py | 62 ++++++++++++- .../agents/memory/hippocampus/context_map.py | 2 +- .../agents/memory/hippocampus/hippocampus.py | 92 ++++++------------- .../hippocampus/modules/cartographer.py | 8 +- .../memory/hippocampus/modules/distiller.py | 8 +- .../agents/reviewer/modules/code_reviewer.py | 18 +--- .../agents/reviewer/modules/doc_extractor.py | 18 +++- .../agents/reviewer/modules/doc_reviewer.py | 16 +--- .../reviewer/modules/scope_identifier.py | 12 +-- .../reviewer/modules/supply_chain_auditor.py | 19 +--- src/codespy/config.py | 42 +++++++-- src/codespy/config_dspy.py | 4 +- src/codespy/config_memory.py | 8 +- src/codespy/tools/storage/s3/server.py | 23 +++++ 17 files changed, 206 insertions(+), 158 deletions(-) diff --git a/.env.example b/.env.example index 251482c..76f3655 100644 --- a/.env.example +++ b/.env.example @@ -186,14 +186,14 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # # 1. Ceiling on the rendered ContextMap. This is the persisted artifact, and it is # prepended to every agent iteration, so it is re-sent ~DEFAULT_MAX_ITERS times -# per scope. Divided by MEMORY_DEFAULT_MAX_ITEM_TOKENS it gives the map's item +# per scope. Divided by MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS it gives the map's item # capacity (3072 / 240 ~= 12 items). # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=3072 # 2. Budget for a SINGLE context-map item, given to the Distiller and the # Cartographer as a prompt input so no one item eats the whole map. Soft limit # (expressed to the LLM, not enforced — truncating an item could corrupt an exact # constant). Lower it for more, terser items; raise it for fewer, richer ones. -# MEMORY_DEFAULT_MAX_ITEM_TOKENS=240 +# MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS=240 # 3. Cap on the trajectory fed to the Distiller. Tool-using agents can produce # 100k+ token trajectories and TwoStepAdapter sends the value twice, so keep this # to ~5-10% of the reflection model's context window. Unset = full trajectory @@ -260,7 +260,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) # - MEMORY_MAX_CONTEXT_MAP_TOKENS (integer) - Ceiling on the persisted ContextMap -# - MEMORY_MAX_ITEM_TOKENS (integer) - Budget for a single context-map item +# - MEMORY_MAX_CONTEXT_ITEM_TOKENS (integer) - Budget for a single context-map item # - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection # - MEMORY_MAX_QUESTION_TOKENS (integer) - Cap on serialized inputs used as the question @@ -275,7 +275,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 # CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 -# CODE_REVIEW_MEMORY_MAX_ITEM_TOKENS=240 +# CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -286,7 +286,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 -# SUPPLY_CHAIN_MEMORY_MAX_ITEM_TOKENS=240 +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -295,7 +295,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 # DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 -# DOC_MEMORY_MAX_ITEM_TOKENS=240 +# DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # DOC_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -306,7 +306,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 # SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 -# SCOPE_MEMORY_MAX_ITEM_TOKENS=240 +# SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # Unused: scope uses question_field="mr_title", so no inputs are serialized. # SCOPE_MEMORY_MAX_QUESTION_TOKENS= diff --git a/codespy.yaml b/codespy.yaml index 7256c15..0587f9c 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -87,9 +87,9 @@ memory: # # 1. max_context_map_tokens — ceiling on the rendered ContextMap. This is the # persisted artifact, and it is prepended to every agent iteration, so it is - # re-sent ~default_max_iters times per scope. Divided by max_item_tokens it + # re-sent ~default_max_iters times per scope. Divided by max_context_item_tokens it # gives the map's item capacity (3072 / 240 ~= 12 items). - # 2. max_item_tokens — budget for a SINGLE context-map item, given to the Distiller + # 2. max_context_item_tokens — budget for a SINGLE context-map item, given to the Distiller # and the Cartographer as a prompt input so no one item eats the whole map. # Soft limit (expressed to the LLM, not enforced — truncating an item could # corrupt an exact constant). Lower it for more, terser items; raise it for @@ -102,7 +102,7 @@ memory: # "question". Without it, every input field is sent in full (for code review that # means the complete patch of every changed file). null = unbounded. default_max_context_map_tokens: 3072 # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS - default_max_item_tokens: 240 # MEMORY_DEFAULT_MAX_ITEM_TOKENS + default_max_context_item_tokens: 240 # MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS default_max_trajectory_tokens: 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS default_max_question_tokens: 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS @@ -206,7 +206,7 @@ signatures: enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS @@ -223,7 +223,7 @@ signatures: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS max_context_map_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: null # CODE_REVIEW_MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS @@ -239,7 +239,7 @@ signatures: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # DOC_MEMORY_MAX_REFLECTS max_context_map_tokens: null # DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: null # DOC_MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: null # DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # DOC_MEMORY_MAX_QUESTION_TOKENS @@ -255,7 +255,7 @@ signatures: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: null # SCOPE_MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS # Unused: scope passes question_field="mr_title", so no inputs are serialized. max_question_tokens: null # SCOPE_MEMORY_MAX_QUESTION_TOKENS @@ -271,7 +271,7 @@ signatures: enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: null # SUMMARIZATION_MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index 79a13f4..11aa664 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -1,3 +1,4 @@ +from codespy.agents.memory.hippocampus.budget import MemoryBudget from codespy.agents.memory.hippocampus.context_map import ( CacheCandidate, ContextMap, @@ -23,6 +24,7 @@ "Hippocampus", "Item", "ItemTag", + "MemoryBudget", "Operation", "OpType", "SectionName", diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index c2b88f8..ced9c46 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -1,5 +1,7 @@ from __future__ import annotations +from dataclasses import dataclass + import dspy import tiktoken @@ -7,6 +9,62 @@ _ENCODING = tiktoken.get_encoding("o200k_base") + +# --------------------------------------------------------------------------- +# Budget +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class MemoryBudget: + """The four token budgets bounding Hippocampus memory. + + These four always travel together: a signature's memory is configured as a + set, not field by field. All are measured with the ``o200k_base`` encoding. + + Frozen because they are read-only configuration — a budget can be resolved + once (see ``Settings.get_memory_budget``) and shared across instances. + + Attributes: + max_context_map_tokens: Hard ceiling on the rendered ContextMap, + enforced by the Evictor after every reflection. This is the + *persisted* artifact and it is prepended to every predictor of the + wrapped agent, so it is re-sent on every agent iteration + (~``max_iters`` times per run) plus once per reflection call — the + most cost-sensitive of the four. Divided by ``max_context_item_tokens`` it + gives the map's approximate item capacity (3072 / 240 ~= 12 items). + max_context_item_tokens: Budget for a *single* context-map item, passed to the + Distiller and the Cartographer as a prompt input so they keep each + item compact rather than spending the whole map budget on one + verbose entry. Unlike the other three this is a **soft** budget: + expressed to the LLM, not enforced in code, since truncating an + item could corrupt an exact constant it holds. Lower it to fit + more, terser items in the same map budget; raise it for richer + items. + max_trajectory_tokens: Budget for trajectories fed to the Distiller. + None = full trajectory, Distiller does all compression — only + viable for agents with short, predictable trajectories. Tool-using + agents can produce 100k+ token trajectories, and TwoStepAdapter + sends the value twice, so prefer ~5–10 % of the Distiller model's + context window (default 8192, i.e. ~5 % of 128k). Applied per call + (stage 1) and again over the combined episode in end_episode() + (stage 2). Step-aware head+tail bounding (60 % head / 40 % tail) + preserves both setup and conclusions. + max_question_tokens: Budget for the serialized inputs used as the + reflection "question", on the fallback path when + ``Hippocampus.question_field`` is None. None = unbounded, which is + rarely safe: *every* input field is serialized, so an agent taking + a large field (a document dump, or a diff of every changed file) + sends all of it to both the Distiller and the Cartographer. + Ignored when ``question_field`` is set — prefer that when a single + field cleanly captures intent. + """ + + max_context_map_tokens: int = 3072 + max_context_item_tokens: int = 240 + max_trajectory_tokens: int | None = 8192 + max_question_tokens: int | None = 2048 + + # Eviction priority — lower number = evict first _SECTION_EVICT_PRIORITY: dict[str, int] = { "parsing_schema": 0, # evict first — cheap to rediscover @@ -34,8 +92,8 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: All fields are included in full. If max_tokens is set, the joined result is head+tail bounded via _head_tail_text so both the instruction and any - trailing intent survive. See Hippocampus.max_question_tokens for guidance on - when and how to set a limit. + trailing intent survive. See MemoryBudget.max_question_tokens for guidance + on when and how to set a limit. """ parts: list[str] = [] for k, v in kwargs.items(): diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py index e62f65b..a8b75f4 100644 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ b/src/codespy/agents/memory/hippocampus/context_map.py @@ -55,7 +55,7 @@ class Item(BaseModel): class CacheCandidate(BaseModel): section: SectionName value: str = Field( - description="Compact candidate cache item, within the max_item_tokens budget." + description="Compact candidate cache item, within the max_context_item_tokens budget." ) transferability: str = Field(description="Kinds of future questions this would help.") rationale: str = Field(description="Why this is shared understanding, not a one-off fact.") diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 0433019..4cd7268 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -8,6 +8,7 @@ import dspy from codespy.agents.memory.hippocampus.budget import ( + MemoryBudget, _head_tail_text, count_tokens, evict, @@ -85,41 +86,26 @@ class Hippocampus(dspy.Module): ## Trajectory bounding (two-stage) - When ``max_trajectory_tokens`` is set: + When ``budget.max_trajectory_tokens`` is set: - **Stage 1** (per call) — each trajectory is head+tail bounded at ``format_trajectory`` time. This keeps the buffer lightweight. - **Stage 2** (``end_episode``) — the joined episode is head+tail bounded again, so the combined result is guaranteed to fit the budget even if many calls are buffered. - With ``max_trajectory_tokens=None`` both stages are no-ops and the Distiller - receives the full trajectory. - - ## The four token budgets - - All four are measured with the ``o200k_base`` encoding: - - - ``max_context_map_tokens`` — the rendered ContextMap. This is the *persisted* - artifact and it is prepended to every predictor of the wrapped agent, so it is - re-sent on every agent iteration (~``max_iters`` times per run) plus once per - reflection call. The most cost-sensitive of the four. - - ``max_item_tokens`` — a *single* context-map item. Unlike the other three this - is a **soft** budget: it is passed to the Distiller and the Cartographer as a - prompt input so they keep each item compact, but it is not enforced in code - (truncating an item could corrupt an exact constant). Roughly, - ``max_context_map_tokens / max_item_tokens`` is the map's item capacity. - - ``max_trajectory_tokens`` — the trajectory fed to the Distiller. - - ``max_question_tokens`` — the serialized inputs used as the reflection - "question" (only when ``question_field`` is unset). + With ``budget.max_trajectory_tokens=None`` both stages are no-ops and the + Distiller receives the full trajectory. + + ## Token budgets + + The four token budgets are grouped into :class:`MemoryBudget`; see that class + for what each one bounds and how to tune it. """ def __init__( self, module: dspy.Module, - max_context_map_tokens: int = 3072, - max_item_tokens: int = 240, - max_trajectory_tokens: int | None = 8192, - max_question_tokens: int | None = 2048, + budget: MemoryBudget | None = None, max_reflects: int | None = None, question_field: str | None = None, task_name: str | None = None, @@ -127,34 +113,10 @@ def __init__( """ Args: module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. - max_context_map_tokens: Hard ceiling on the rendered context map, - enforced by the Evictor after every reflection. Divided by - ``max_item_tokens`` it gives the map's approximate item capacity - (3072 / 240 ~= 12 items). Raise with care: the map is re-sent on - every agent iteration, so the cost is multiplied by ``max_iters``. - max_item_tokens: Token budget for a *single* context-map item, passed to - the Distiller and the Cartographer as a prompt input so they keep - each item compact rather than spending the whole map budget on one - verbose entry. Soft limit — expressed to the LLM, not enforced in - code, since truncating an item could corrupt an exact constant it - holds. Lower it to fit more, terser items in the same map budget; - raise it to allow richer items. - max_trajectory_tokens: Token budget for trajectories fed to the Distiller. - None = full trajectory, Distiller does all compression — only viable - for agents with short, predictable trajectories. Tool-using agents - can produce 100k+ token trajectories, and TwoStepAdapter sends the - value twice, so prefer ~5–10 % of the Distiller model's context - window (default 8192, i.e. ~5 % of 128k). Applied per call (stage 1) - and again over the combined episode in end_episode() (stage 2). - Step-aware head+tail bounding (60 % head / 40 % tail) preserves both - setup and conclusions. - max_question_tokens: Token budget for the serialized inputs used as the - reflection "question", on the fallback path when question_field is - None. None = unbounded, which is rarely safe: *every* input field is - serialized, so an agent taking a large field (a document dump, or a - diff of every changed file) sends all of it to both the Distiller and - the Cartographer. Ignored when question_field is set — prefer that - when a single field cleanly captures intent. + budget: The four token budgets bounding memory, as a + :class:`MemoryBudget`. Defaults to ``MemoryBudget()`` — see that + class for per-field guidance. Resolve one from configuration with + ``Settings.get_memory_budget(signature_name)``. max_reflects: Maximum number of forward() calls that also reflect online. None (default): no limit — reflect after every call (classic online learning). 0: never reflect online — pure buffering until end_episode(). @@ -163,7 +125,8 @@ def __init__( end_episode() is always available. question_field: Name of the input field carrying the task description. If set, only that field is used as the Distiller "question". - If None, all input fields are serialized (bounded by max_question_tokens). + If None, all input fields are serialized (bounded by + ``budget.max_question_tokens``). Set this when one field cleanly captures user intent. task_name: Identity recorded in ``Episode.task`` and used in the episode filename. Pass the signature's snake_case name (``"doc"``, @@ -194,10 +157,7 @@ def __init__( self.agent = module self.distill = Distiller() self.cartograph = Cartographer() - self.max_context_map_tokens = max_context_map_tokens - self.max_item_tokens = max_item_tokens - self.max_trajectory_tokens = max_trajectory_tokens - self.max_question_tokens = max_question_tokens + self.budget = budget or MemoryBudget() self.max_reflects = max_reflects self.question_field = question_field self.cmap = ContextMap() @@ -249,7 +209,7 @@ def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None: Buffers the (stage-1 bounded) trajectory and, depending on ``max_reflects``, runs an online distill+apply pass immediately. """ - traj = format_trajectory(pred, self.max_trajectory_tokens) + traj = format_trajectory(pred, self.budget.max_trajectory_tokens) self._episode_trajectories.append(traj) if self._episode_question is None: self._episode_question = self._make_question(kwargs) @@ -272,8 +232,8 @@ def _consolidate(self) -> str | None: f"=== Call {i + 1} ===\n{t}" for i, t in enumerate(self._episode_trajectories) ) - if self.max_trajectory_tokens is not None: - combined = _head_tail_text(combined, self.max_trajectory_tokens) + if self.budget.max_trajectory_tokens is not None: + combined = _head_tail_text(combined, self.budget.max_trajectory_tokens) self._distill(combined, self._episode_question or "") return combined @@ -312,7 +272,7 @@ def end_episode( """Consolidate the buffered trajectories into the map and record an Episode snapshot. A single Distiller pass sees all buffered trajectories joined with - ``=== Call k ===`` headers. If ``max_trajectory_tokens`` is set, the + ``=== Call k ===`` headers. If ``budget.max_trajectory_tokens`` is set, the combined text is head+tail bounded (stage 2) after per-call bounding (stage 1) already applied at append time. The question is derived from the first buffered call. No-op if the buffer is empty. @@ -417,14 +377,14 @@ def reset_episode(self) -> None: def _make_question(self, inputs: dict) -> str: if self.question_field is not None: return str(inputs.get(self.question_field, "")) - return format_inputs(inputs, self.max_question_tokens) + return format_inputs(inputs, self.budget.max_question_tokens) def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, context_map=self.cmap, question=question, - max_item_tokens=self.max_item_tokens, + max_context_item_tokens=self.budget.max_context_item_tokens, ) known = self.cmap.ids() @@ -445,9 +405,9 @@ def _distill(self, trajectory: str, question: str) -> None: question=question, # The Cartographer's input field keeps the generic name: it is prompt # text, already scoped by its description, and pairs with current_tokens. - token_budget=self.max_context_map_tokens, + token_budget=self.budget.max_context_map_tokens, current_tokens=count_tokens(self.cmap.render()), - max_item_tokens=self.max_item_tokens, + max_context_item_tokens=self.budget.max_context_item_tokens, ) ops = list(edits.operations or []) @@ -456,7 +416,7 @@ def _distill(self, trajectory: str, question: str) -> None: for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 - self.cmap = evict(self.cmap, self.scores, self.max_context_map_tokens) + self.cmap = evict(self.cmap, self.scores, self.budget.max_context_map_tokens) live = self.cmap.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index fcb2825..ec85a08 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -33,7 +33,7 @@ class CartographerSig(dspy.Signature): Prefer REPLACE over ADD when possible. - Add new items only when they represent transferable understanding. - Each item must be short and budget-efficient — stay within the - `max_item_tokens` budget given as an input. If a candidate exceeds + `max_context_item_tokens` budget given as an input. If a candidate exceeds it, rewrite it more compactly or split it. - If nothing new is worth keeping, return an empty operations list. @@ -110,7 +110,7 @@ class CartographerSig(dspy.Signature): question: str = dspy.InputField(desc="Question the agent was answering.") token_budget: int = dspy.InputField(desc="Hard token budget for the context map.") current_tokens: int = dspy.InputField(desc="Current token count of the context map.") - max_item_tokens: int = dspy.InputField( + max_context_item_tokens: int = dspy.InputField( desc="Token budget for a SINGLE context-map item. Every ADD/REPLACE content " "must stay within it." ) @@ -143,7 +143,7 @@ def __init__(self): self.predict = dspy.ChainOfThought(CartographerSig) def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, - token_budget, current_tokens, max_item_tokens): + token_budget, current_tokens, max_context_item_tokens): # See Distiller.forward: SignatureContext applies memory.cartographer's # LLM settings and gives this module its own cost line. Entered here # because DSPy's context is thread-scoped and reflection runs in a worker. @@ -158,6 +158,6 @@ def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, question=question, token_budget=token_budget, current_tokens=current_tokens, - max_item_tokens=max_item_tokens, + max_context_item_tokens=max_context_item_tokens, ) diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 9df9f1b..68a4cb7 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -109,7 +109,7 @@ class DistillerSig(dspy.Signature): trajectory: str = dspy.InputField(desc="The agent's full execution trajectory.") context_map: ContextMap = dspy.InputField(desc="Current context map (with item IDs).") question: str = dspy.InputField(desc="The question the agent was answering.") - max_item_tokens: int = dspy.InputField( + max_context_item_tokens: int = dspy.InputField( desc="Token budget for a SINGLE context-map item. Keep every candidate within " "it; if one exceeds it, rewrite it more compactly or split it." ) @@ -124,7 +124,7 @@ class DistillerSig(dspy.Signature): "Keys must match existing item ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( - desc="Candidate items to add. Each within the max_item_tokens budget; " + desc="Candidate items to add. Each within the max_context_item_tokens budget; " "structural/transferable only. " "Each candidate's `section` must be one of the five section names above." ) @@ -151,7 +151,7 @@ def forward( trajectory: str, context_map: ContextMap, question: str, - max_item_tokens: int, + max_context_item_tokens: int, ): # SignatureContext applies memory.distiller's model/temperature/reasoning # effort and attributes the cost to this module rather than to whichever @@ -165,6 +165,6 @@ def forward( trajectory=trajectory, context_map=context_map, question=question, - max_item_tokens=max_item_tokens, + max_context_item_tokens=max_context_item_tokens, ) diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index a4a4cd5..65acdbd 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -232,22 +232,12 @@ async def aforward( ) async with SignatureContext("code_review", self._cost_tracker): if self._settings.get_memory_enabled("code_review"): + # No question_field: the scope (with every patch) is the + # only input, so budget.max_question_tokens must bound + # the serialized question. mem = Hippocampus( agent, - max_context_map_tokens=( - self._settings.get_memory_max_context_map_tokens("code_review") - ), - max_item_tokens=self._settings.get_memory_max_item_tokens( - "code_review" - ), - max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( - "code_review" - ), - # No question_field: the scope (with every patch) is the - # only input, so the serialized question must be bounded. - max_question_tokens=self._settings.get_memory_max_question_tokens( - "code_review" - ), + budget=self._settings.get_memory_budget("code_review"), max_reflects=self._settings.get_memory_max_reflects("code_review"), task_name="code_review", ) diff --git a/src/codespy/agents/reviewer/modules/doc_extractor.py b/src/codespy/agents/reviewer/modules/doc_extractor.py index b87cb12..81ddc6c 100644 --- a/src/codespy/agents/reviewer/modules/doc_extractor.py +++ b/src/codespy/agents/reviewer/modules/doc_extractor.py @@ -72,8 +72,24 @@ def extract_documentation(scope_root: Path) -> str: for path in doc_paths: try: content = fs.read_file(path) - parts.append(f"=== {path} ===\n{content.content}") except Exception as e: # noqa: BLE001 + # read_file returns an error Content rather than raising for missing + # or unreadable files, but path resolution and stat() can still raise. logger.warning(f"Could not read doc file {path}: {e}") + continue + + # read_file signals failure via Content.error, leaving content empty. An + # unchecked append would emit a header with a blank body, which reads to + # the LLM as "this doc exists and is empty" rather than "not available" + # — prompting false "add missing docs" findings. + if not content.success: + logger.debug(f"Skipping unreadable doc file {path}: {content.error}") + continue + + if not content.content.strip(): + logger.debug(f"Skipping empty doc file {path}") + continue + + parts.append(f"=== {path} ===\n{content.content}") return "\n\n".join(parts) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 6d12c9d..eda94ee 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -164,20 +164,12 @@ async def aforward( ) async with SignatureContext("doc", self._cost_tracker): if self._settings.get_memory_enabled("doc"): + # No question_field: patches + documentation are both + # large, so budget.max_question_tokens must bound the + # serialized question. mem = Hippocampus( reviewer, - max_context_map_tokens=( - self._settings.get_memory_max_context_map_tokens("doc") - ), - max_item_tokens=self._settings.get_memory_max_item_tokens("doc"), - max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( - "doc" - ), - # No question_field: patches + documentation are both - # large, so the serialized question must be bounded. - max_question_tokens=self._settings.get_memory_max_question_tokens( - "doc" - ), + budget=self._settings.get_memory_budget("doc"), max_reflects=self._settings.get_memory_max_reflects("doc"), # ChainOfThought exposes no .signature, so the episode # identity must be given explicitly. diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 0dfa17a..76b217f 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -262,16 +262,10 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal if self._settings.get_memory_enabled("scope"): mem = Hippocampus( agent, - max_context_map_tokens=( - self._settings.get_memory_max_context_map_tokens("scope") - ), - max_item_tokens=self._settings.get_memory_max_item_tokens("scope"), - max_trajectory_tokens=self._settings.get_memory_max_trajectory_tokens( - "scope" - ), + budget=self._settings.get_memory_budget("scope"), max_reflects=self._settings.get_memory_max_reflects("scope"), - # question_field makes max_question_tokens moot: the title - # alone is the question, so no inputs get serialized. + # question_field makes budget.max_question_tokens moot: the + # title alone is the question, so no inputs get serialized. question_field="mr_title", task_name="scope", ) diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index fa15f48..454ee97 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -311,24 +311,7 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list if self._settings.get_memory_enabled("supply_chain"): mem = Hippocampus( supply_chain_agent, - max_context_map_tokens=( - self._settings.get_memory_max_context_map_tokens( - "supply_chain" - ) - ), - max_item_tokens=( - self._settings.get_memory_max_item_tokens("supply_chain") - ), - max_trajectory_tokens=( - self._settings.get_memory_max_trajectory_tokens( - "supply_chain" - ) - ), - max_question_tokens=( - self._settings.get_memory_max_question_tokens( - "supply_chain" - ) - ), + budget=self._settings.get_memory_budget("supply_chain"), max_reflects=self._settings.get_memory_max_reflects( "supply_chain" ), diff --git a/src/codespy/config.py b/src/codespy/config.py index c0de321..891f4c8 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -2,7 +2,8 @@ import logging from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any + import yaml from pydantic import Field, model_validator @@ -40,6 +41,12 @@ reset_memory_store, ) +if TYPE_CHECKING: + # Imported lazily inside get_memory_budget(): importing this at module level + # pulls in codespy.agents, whose __init__ imports dspy_config, which imports + # this module — a circular import that breaks every entrypoint. + from codespy.agents.memory.hippocampus.budget import MemoryBudget + @@ -269,8 +276,8 @@ def get_memory_max_context_map_tokens(self, signature_name: str) -> int: else self.memory.default_max_context_map_tokens ) - def get_memory_max_item_tokens(self, signature_name: str) -> int: - """Get max_item_tokens for a signature's memory (signature-specific or default). + def get_memory_max_context_item_tokens(self, signature_name: str) -> int: + """Get max_context_item_tokens for a signature's memory (signature-specific or default). Bounds a *single* context-map item. Handed to the Distiller and the Cartographer as a prompt input so they keep each item compact instead of @@ -279,9 +286,9 @@ def get_memory_max_item_tokens(self, signature_name: str) -> int: """ config = self.get_signature_config(signature_name).memory return ( - config.max_item_tokens - if config.max_item_tokens is not None - else self.memory.default_max_item_tokens + config.max_context_item_tokens + if config.max_context_item_tokens is not None + else self.memory.default_max_context_item_tokens ) def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: @@ -309,6 +316,29 @@ def get_memory_max_question_tokens(self, signature_name: str) -> int | None: else self.memory.default_max_question_tokens ) + def get_memory_budget(self, signature_name: str) -> "MemoryBudget": + """Resolve the full ``MemoryBudget`` for a signature's memory. + + Composes the four per-field getters, so each budget still resolves as + "signature-specific override, else ``memory.default_*``". Pass the result + straight to ``Hippocampus(module, budget=...)``. + + Args: + signature_name: The signature whose memory budget to resolve. + + Returns: + A fully resolved ``MemoryBudget`` (no None-means-default fields). + """ + # Deferred import — see the TYPE_CHECKING note at the top of this module. + from codespy.agents.memory.hippocampus.budget import MemoryBudget + + return MemoryBudget( + max_context_map_tokens=self.get_memory_max_context_map_tokens(signature_name), + max_context_item_tokens=self.get_memory_max_context_item_tokens(signature_name), + max_trajectory_tokens=self.get_memory_max_trajectory_tokens(signature_name), + max_question_tokens=self.get_memory_max_question_tokens(signature_name), + ) + def log_signature_configs(self) -> None: """Log all signature and reflection module LLM configurations.""" logger.info("Signature configurations:") diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index f5cc7b0..d1a267f 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -27,7 +27,7 @@ class MemorySignatureConfig(BaseModel): enabled: bool | None = None # _MEMORY_ENABLED max_reflects: int | None = None # _MEMORY_MAX_REFLECTS max_context_map_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MAP_TOKENS - max_item_tokens: int | None = None # _MEMORY_MAX_ITEM_TOKENS + max_context_item_tokens: int | None = None # _MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: int | None = None # _MEMORY_MAX_QUESTION_TOKENS @@ -94,7 +94,7 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - ``SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS`` -> signatures.scope.memory.max_context_map_tokens - - ``SCOPE_MEMORY_MAX_ITEM_TOKENS`` -> signatures.scope.memory.max_item_tokens + - ``SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS`` -> signatures.scope.memory.max_context_item_tokens Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) are handled directly by pydantic-settings and should NOT be processed here. diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 6fc1f5c..dcbb215 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -83,7 +83,7 @@ class MemoryConfig(BaseModel): # every ReAct iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. # Approximate item capacity is default_max_context_map_tokens divided by - # default_max_item_tokens (3072 / 240 ~= 12 items). + # default_max_context_item_tokens (3072 / 240 ~= 12 items). # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS default_max_context_map_tokens: int = Field(default=3072) @@ -92,8 +92,8 @@ class MemoryConfig(BaseModel): # budget on one verbose entry. Soft limit: it is expressed to the LLM rather # than enforced in code (truncating an item could corrupt an exact constant). # The hard, map-wide limit is default_max_context_map_tokens, enforced by the - # Evictor. MEMORY_DEFAULT_MAX_ITEM_TOKENS - default_max_item_tokens: int = Field(default=240) + # Evictor. MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS + default_max_context_item_tokens: int = Field(default=240) # Head+tail cap on the agent trajectory fed to the Distiller. Without it a @@ -133,7 +133,7 @@ class MemoryConfig(BaseModel): "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", "DEFAULT_MAX_CONTEXT_MAP_TOKENS": "default_max_context_map_tokens", - "DEFAULT_MAX_ITEM_TOKENS": "default_max_item_tokens", + "DEFAULT_MAX_CONTEXT_ITEM_TOKENS": "default_max_context_item_tokens", "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", "DEFAULT_MAX_QUESTION_TOKENS": "default_max_question_tokens", } diff --git a/src/codespy/tools/storage/s3/server.py b/src/codespy/tools/storage/s3/server.py index 2d43874..9e4431a 100644 --- a/src/codespy/tools/storage/s3/server.py +++ b/src/codespy/tools/storage/s3/server.py @@ -145,6 +145,27 @@ def read_file(path: str, max_bytes: int = 100_000, max_lines: int | None = None) return dict(_read_file_cached(path, max_bytes, max_lines)) +# ------------------------------------------------------------------ +# Cache invalidation +# ------------------------------------------------------------------ + + +def _invalidate_read_caches() -> None: + """Clear all cached read results after a mutating operation. + + lru_cache has no per-key eviction, and a single write or delete can + invalidate entries across several caches - including listings and trees + for every ancestor prefix - so all read caches are cleared wholesale. + Called unconditionally, since a failed put/delete may still have changed + bucket state (timeouts, partial uploads, ambiguous retries). + """ + _file_exists_cached.cache_clear() + _get_file_info_cached.cache_clear() + _list_directory_cached.cache_clear() + _get_tree_cached.cache_clear() + _read_file_cached.cache_clear() + + # ------------------------------------------------------------------ # Write tools (not cached) # ------------------------------------------------------------------ @@ -165,6 +186,7 @@ def write_file(path: str, content: str, content_type: str = "text/plain") -> dic client = _get_client() logger.info(f"[S3] {_caller_module} -> write_file: s3://{client.bucket}/{path}") result = client.write_file(path, content, content_type) + _invalidate_read_caches() return result.model_dump() @@ -181,6 +203,7 @@ def delete_file(path: str) -> dict: client = _get_client() logger.info(f"[S3] {_caller_module} -> delete_file: s3://{client.bucket}/{path}") result = client.delete_file(path) + _invalidate_read_caches() return result.model_dump() From 1acdbadc5e213132480ee867f1e0ac098dc1ea24 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Wed, 5 Aug 2026 01:24:48 +0200 Subject: [PATCH 24/79] wip --- .env.example | 11 +++- codespy.yaml | 12 ++-- .../agents/memory/hippocampus/context_map.py | 48 ++------------ .../agents/memory/hippocampus/episode.py | 32 +++++++-- .../agents/memory/hippocampus/hippocampus.py | 54 ++++++++++++--- src/codespy/agents/reviewer/models.py | 5 ++ .../agents/reviewer/modules/code_reviewer.py | 41 ++++++++---- .../agents/reviewer/modules/doc_reviewer.py | 36 +++++++--- .../agents/reviewer/modules/helpers.py | 39 +++++++++++ .../reviewer/modules/scope_identifier.py | 64 ++++++++++++++++-- .../reviewer/modules/supply_chain_auditor.py | 45 ++++++++++--- src/codespy/agents/reviewer/reviewer.py | 65 +++++++++++++++---- 12 files changed, 341 insertions(+), 111 deletions(-) diff --git a/.env.example b/.env.example index 76f3655..eb74af9 100644 --- a/.env.example +++ b/.env.example @@ -161,7 +161,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # the per-signature MEMORY_* settings below. # # Episodes are written to: -# episodes///codespy--.json +# global/episodic///codespy--.json # under MEMORY_ROOT (filesystem) or MEMORY_S3_BUCKET (s3). # Storage backend: filesystem or s3 (default: filesystem) @@ -313,5 +313,10 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUMMARIZATION_ENABLED=true # SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 -# Memory is not wired for summarization (no tools/scope) — leave disabled -# SUMMARIZATION_MEMORY_ENABLED=false +# SUMMARIZATION_MEMORY_ENABLED=true +# SUMMARIZATION_MEMORY_MAX_REFLECTS=1 +# SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SUMMARIZATION_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS=2048 + diff --git a/codespy.yaml b/codespy.yaml index 0587f9c..8516b27 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -61,12 +61,14 @@ gitlab: # MEMORY # ============================================================================ # Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# consolidate their run into a ContextMap and persist it as an Episode. -# Save-only for now (no loading). Disabled by default per-signature — see -# `memory:` blocks under each signature below. +# and the summarization step consolidate their run into a ContextMap and +# persist it as an Episode. Save-only for now (no loading). Disabled by +# default globally; enabled by default for summarization — see `memory:` +# blocks under each signature below. + # # Episodes are written to: -# episodes///codespy--.json +# global/episodic///codespy--.json # under `root` (filesystem) or `s3_bucket` (s3). memory: backend: filesystem # MEMORY_BACKEND (filesystem | s3) @@ -268,7 +270,7 @@ signatures: temperature: null # SUMMARIZATION_TEMPERATURE max_tokens: null # SUMMARIZATION_MAX_TOKENS memory: - enabled: false # SUMMARIZATION_MEMORY_ENABLED — not wired (no tools/scope) + enabled: true # SUMMARIZATION_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS max_context_map_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS max_context_item_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_ITEM_TOKENS diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py index a8b75f4..51abfea 100644 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ b/src/codespy/agents/memory/hippocampus/context_map.py @@ -1,5 +1,6 @@ from __future__ import annotations +import uuid from enum import Enum from typing import Literal @@ -98,11 +99,10 @@ class ContextMap(BaseModel): "that multiple questions would need" ), ) - next_id: int = Field(default=1, exclude=True) @classmethod def section_names(cls) -> list[str]: - return [n for n in cls.model_fields if n != "next_id"] + return list(cls.model_fields) def section(self, name: str) -> list[Item]: return getattr(self, name) @@ -143,10 +143,9 @@ def apply(self, ops: list[Operation]) -> tuple[ContextMap, list[str]]: lst[i] = Item(id=it.id, content=op.content) elif op.type == OpType.ADD and op.section and op.content: prefix = _SECTION_PREFIX.get(op.section, op.section[:2]) - new_id = f"{prefix}-{cm.next_id:05d}" + new_id = f"{prefix}-{uuid.uuid4().hex}" cm.section(op.section).append(Item(id=new_id, content=op.content)) new_ids.append(new_id) - cm.next_id += 1 return cm, new_ids def without(self, ids: set[str]) -> ContextMap: @@ -157,45 +156,10 @@ def without(self, ids: set[str]) -> ContextMap: return cm def to_json(self) -> str: - """Serialize the map to a JSON string. - - ``next_id`` is excluded from the output (it is a transient counter). - Use ``from_json()`` to reload — it recomputes ``next_id`` from the - item IDs present in the map so there are no collisions on subsequent - ADD operations. - """ + """Serialize the map to a JSON string.""" return self.model_dump_json(indent=2) @classmethod def from_json(cls, text: str) -> ContextMap: - """Deserialize a map from a JSON string produced by ``to_json()``. - - Recomputes ``next_id`` as one past the highest numeric suffix found - in any item ID (e.g. ``cu-00042`` → suffix 42), so the reloaded map - can safely receive further ADD operations without ID collisions. - """ - cm = cls.model_validate_json(text) - max_n = 0 - for it in cm.all_items(): - suffix = it.id.rsplit("-", 1)[-1] - if suffix.isdigit(): - max_n = max(max_n, int(suffix)) - cm.next_id = max_n + 1 - return cm - -def _recompute_next_id(cmap: ContextMap) -> None: - """Recompute ``cmap.next_id`` from the highest numeric item-ID suffix (in-place). - - After deserialisation the ``next_id`` counter is reset from the highest - numeric suffix found in any item ID (e.g. ``cu-00042`` → 42), so the - restored map can safely receive further ADD operations without collisions. - - Args: - cmap: The context map to update in-place. - """ - max_n = 0 - for item in cmap.all_items(): - suffix = item.id.rsplit("-", 1)[-1] - if suffix.isdigit(): - max_n = max(max_n, int(suffix)) - cmap.next_id = max_n + 1 + """Deserialize a map from a JSON string produced by ``to_json()``.""" + return cls.model_validate_json(text) diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index efec582..5001abc 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field -from codespy.agents.memory.hippocampus.context_map import ContextMap, _recompute_next_id +from codespy.agents.memory.hippocampus.context_map import ContextMap from codespy.tools.storage.base import Storage @@ -24,19 +24,41 @@ class Episode(BaseModel): ``"CodeReviewSignature"``). Falls back to the module class name when the wrapped module exposes no signature. module: Class name of the wrapped ``dspy.Module`` (e.g. ``"CodeReviewer"``). + question: Question/task description derived from the first buffered + call's inputs (via ``question_field`` or serialized input fields). context_map: Deep-copied snapshot of the context map *after* consolidation, so later edits to the live map do not mutate this record. timestamp: UTC time the episode was recorded. + artifacts: Named output artifacts produced by the wrapped agent for + this episode (e.g. ``{"review": ""}``). Agent-agnostic: + any module can attach whatever markdown/text output it produced + under a key of its choosing. Empty by default. + run_id: Identifier of the pipeline run that produced this episode. + Shared by every agent/module invoked within the same + ``ReviewPipeline.forward()`` call, so all episodes from one + review run can be correlated. Also used as the ```` suffix + in the episode filename: ``-.json``. """ - + run_id: str = Field( + default="", + description=( + "Identifier of the pipeline run that produced this episode. " + "Shared across all agents invoked within the same review run." + ), + ) task: str = Field(description="Wrapped signature name (or module class name as fallback)") module: str = Field(description="Wrapped dspy.Module class name") + question: str = Field(description="Question/task description for this episode") context_map: ContextMap = Field(description="Consolidated context map snapshot") timestamp: datetime = Field( default_factory=lambda: datetime.now(UTC), description="UTC time the episode was recorded", ) + artifacts: dict[str, str] = Field( + default_factory=dict, + description="Named output artifacts produced by the agent (e.g. {'review': ''})", + ) def save_episode(store: Storage, path: str, episode: Episode) -> None: @@ -60,15 +82,12 @@ def save_episode(store: Storage, path: str, episode: Episode) -> None: def load_episode(store: Storage, path: str) -> Episode: """Load an episode from ``path`` via ``store``. - The embedded ``ContextMap.next_id`` is recomputed from the loaded item IDs - so the restored map can safely receive further ADD operations. - Args: store: A ``FileSystem`` or ``S3Client`` instance. path: Source path (relative to the store's root / bucket). Returns: - An ``Episode`` with ``context_map.next_id`` recomputed. + The loaded ``Episode``. Raises: FileNotFoundError: If the path does not exist in the store. @@ -86,5 +105,4 @@ def load_episode(store: Storage, path: str) -> Episode: episode = Episode.model_validate_json(result.content) except Exception as exc: raise OSError(f"Failed to parse episode from {path!r}: {exc}") from exc - _recompute_next_id(episode.context_map) return episode diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 4cd7268..b1356ae 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -109,6 +109,7 @@ def __init__( max_reflects: int | None = None, question_field: str | None = None, task_name: str | None = None, + run_id: str | None = None, ): """ Args: @@ -136,6 +137,12 @@ class for per-field guidance. Resolve one from configuration with ``dspy.ReAct``-style modules expose ``.signature``, ``dspy.ChainOfThought`` does not, so the fallback would yield a meaningless (and collision-prone) ``"ChainOfThought"``. + run_id: Identifier of the pipeline run this agent belongs to. Passed + down by the orchestrating ``ReviewPipeline`` so every module + invoked within the same review run shares the same identifier, + used as the ```` prefix in the episode filename + (``-.json``) and recorded on ``Episode.run_id``. + If ``None`` (standalone usage), a random UUID is generated. """ super().__init__() @@ -178,6 +185,10 @@ class for per-field guidance. Resolve one from configuration with top_sig.__name__ if top_sig is not None else type(module).__name__ ) self._module_name: str = type(module).__name__ + # Identifier of the pipeline run this agent belongs to (see run_id arg + # above). Falls back to a random UUID for standalone usage where no + # orchestrator provides one. + self._run_id: str = run_id or uuid.uuid4().hex # The most recent consolidated Episode; set by end_episode(), None until then. self.episode: Episode | None = None @@ -237,37 +248,48 @@ def _consolidate(self) -> str | None: self._distill(combined, self._episode_question or "") return combined - def _finalize_episode(self) -> None: - """Record the consolidated Episode snapshot and clear the buffer.""" + def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: + """Record the consolidated Episode snapshot and clear the buffer. + + Args: + artifacts: Named output artifacts to attach to the recorded + episode (e.g. ``{"review": ""}``). Defaults to an + empty dict when omitted. + """ self.episode = Episode( task=self._task_name, module=self._module_name, + question=self._episode_question or "", context_map=self.cmap.model_copy(deep=True), timestamp=datetime.now(UTC), + artifacts=artifacts or {}, + run_id=self._run_id, ) self._episode_trajectories.clear() self._episode_question = None self._reflected_count = 0 + def _episode_file_path(self, dir: str) -> str: """Build the full episode file path from a directory. Prepends the ``episodes`` root and appends a hidden ``.codespy`` - folder holding the episode file, named after the wrapped task and a - random UUID: ``episodes//.codespy/-.json``. + folder holding the episode file, named after the pipeline run's + identifier and the wrapped task: + ``global/episodic//.codespy/-.json``. Args: dir: Directory identifying where this episode belongs (e.g. a scope's ``/{repo}/{subroot}/`` path). """ - file_id = uuid.uuid4().hex trimmed = dir.strip("/") - return f"episodes/{trimmed}/.codespy/{self._task_name}-{file_id}.json" + return f"global/episodic/{trimmed}/.codespy/{self._run_id}-{self._task_name}.json" def end_episode( self, store: Storage | None = None, dir: str | None = None, + artifacts: dict[str, str] | None = None, ) -> None: """Consolidate the buffered trajectories into the map and record an Episode snapshot. @@ -283,7 +305,7 @@ def end_episode( If both ``store`` and ``dir`` are provided the episode is persisted via ``save_episode()`` after consolidation, at - ``episodes//.codespy/-.json``. ``store`` may be a + ``global/episodic//.codespy/-.json``. ``store`` may be a ``FileSystem`` or an ``S3Client`` instance. Args: @@ -291,6 +313,10 @@ def end_episode( consolidation (``FileSystem`` or ``S3Client``). dir: Directory identifying where this episode belongs (e.g. a scope's path). Required when ``store`` is set. + artifacts: Named output artifacts to attach to the recorded + episode (e.g. ``{"review": ""}``). Agent-agnostic — + any caller can attach whatever markdown/text output it + produced under a key of its choosing. Raises: OSError: If persistence is requested and the write fails. @@ -298,7 +324,7 @@ def end_episode( nothing_to_persist = self._consolidate() is None and self._reflected_count==0 if nothing_to_persist: return - self._finalize_episode() + self._finalize_episode(artifacts) if store is not None and dir is not None: _save_episode(store, self._episode_file_path(dir), self.episode) @@ -306,22 +332,32 @@ async def aend_episode( self, store: Storage | None = None, dir: str | None = None, + artifacts: dict[str, str] | None = None, ) -> None: """Async counterpart of :meth:`end_episode`. The (synchronous) Distiller/Cartographer consolidation pass and the storage write are both offloaded to a thread so they never block the caller's event loop. + + Args: + store: Optional ``Storage`` backend to persist the episode after + consolidation (``FileSystem`` or ``S3Client``). + dir: Directory identifying where this episode belongs (e.g. a + scope's path). Required when ``store`` is set. + artifacts: Named output artifacts to attach to the recorded + episode (e.g. ``{"review": ""}``). """ combined = await asyncio.to_thread(self._consolidate) nothing_to_persist = combined is None and self._reflected_count == 0 if nothing_to_persist: return - await asyncio.to_thread(self._finalize_episode) + await asyncio.to_thread(self._finalize_episode, artifacts) if store is not None and dir is not None: path = self._episode_file_path(dir) await asyncio.to_thread(_save_episode, store, path, self.episode) + def save_episode(self, store: Storage, path: str) -> None: """Persist the current episode to ``path`` via ``store``. diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 4c6fa0f..b3a33db 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -148,6 +148,11 @@ class ReviewResult(BaseModel): mr_title: str = Field(description="MR title") mr_url: str = Field(description="MR URL") repo: str = Field(description="Repository name (owner/repo)") + run_id: str = Field( + default="", + description="Identifier of the pipeline run that produced this result, " + "shared with all Episode records persisted during this run", + ) reviewed_at: datetime = Field( default_factory=lambda: datetime.now(UTC), description="Review timestamp", diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 65acdbd..6d47547 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -12,10 +12,12 @@ from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, + issues_to_markdown, make_scope_relative, resolve_scope_root, restore_repo_paths, ) + from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -143,9 +145,6 @@ class CodeReviewSignature(dspy.Signature): class CodeReviewer(dspy.Module): """Unified code reviewer — defects, security, and smells in a single pass. - Merges DefectDetector and SmellDetector into one agent to avoid redundant - README reads, tool sessions, and input token costs per scope. - MCP tools are scope-restricted: for each scope, tools are rooted at repo_path/scope.subroot so the agent cannot access files outside the scope. """ @@ -181,13 +180,18 @@ async def _create_tools( return tools, contexts async def aforward( - self, scopes: Sequence[ScopeResult], repo_path: Path + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, ) -> list[Issue]: """Analyze scopes for defects, security issues, and code smells. Args: scopes: List of identified scopes with their changed files repo_path: Path to the cloned repository + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run (see ``Hippocampus.run_id``) Returns: List of bug, security, and smell issues found across all scopes @@ -240,22 +244,30 @@ async def aforward( budget=self._settings.get_memory_budget("code_review"), max_reflects=self._settings.get_memory_max_reflects("code_review"), task_name="code_review", + run_id=run_id, ) result = await mem.aforward( scope=scoped, categories=categories, ) - await mem.aend_episode(get_memory_store(self._settings), scope.scope_path()) + issues = [ + issue for issue in (result.issues or []) + if issue.confidence >= MIN_CONFIDENCE + ] + await mem.aend_episode( + get_memory_store(self._settings), + scope.scope_path(), + artifacts={"review": issues_to_markdown(issues)}, + ) else: result = await agent.acall( scope=scoped, categories=categories, ) - - issues = [ - issue for issue in (result.issues or []) - if issue.confidence >= MIN_CONFIDENCE - ] + issues = [ + issue for issue in (result.issues or []) + if issue.confidence >= MIN_CONFIDENCE + ] restore_repo_paths(issues, scope.subroot) all_issues.extend(issues) logger.debug( @@ -270,15 +282,20 @@ async def aforward( return all_issues def forward( - self, scopes: Sequence[ScopeResult], repo_path: Path + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, ) -> list[Issue]: """Analyze scopes for code issues (sync wrapper). Args: scopes: List of identified scopes with their changed files repo_path: Path to the cloned repository + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run Returns: List of bug, security, and smell issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index eda94ee..639e47d 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -13,6 +13,7 @@ from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, + issues_to_markdown, make_scope_relative, resolve_scope_root, restore_repo_paths, @@ -112,13 +113,18 @@ def _build_patches(self, scope: ScopeResult) -> str: return "\n\n".join(parts) async def aforward( - self, scopes: Sequence[ScopeResult], repo_path: Path + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, ) -> list[Issue]: """Analyze scopes for documentation issues. Args: scopes: List of identified scopes with their changed files repo_path: Path to the cloned repository + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run (see ``Hippocampus.run_id``) Returns: List of documentation issues found across all scopes @@ -174,13 +180,22 @@ async def aforward( # ChainOfThought exposes no .signature, so the episode # identity must be given explicitly. task_name="doc", + run_id=run_id, ) result = await mem.aforward( patches=patches, documentation=documentation, categories=[IssueCategory.DOCUMENTATION], ) - await mem.aend_episode(get_memory_store(self._settings), scope.scope_path()) + issues = [ + issue for issue in (result.issues or []) + if issue.confidence >= MIN_CONFIDENCE + ] + await mem.aend_episode( + get_memory_store(self._settings), + scope.scope_path(), + artifacts={"review": issues_to_markdown(issues)}, + ) else: result = await asyncio.to_thread( reviewer, @@ -188,10 +203,10 @@ async def aforward( documentation=documentation, categories=[IssueCategory.DOCUMENTATION], ) - issues = [ - issue for issue in (result.issues or []) - if issue.confidence >= MIN_CONFIDENCE - ] + issues = [ + issue for issue in (result.issues or []) + if issue.confidence >= MIN_CONFIDENCE + ] restore_repo_paths(issues, scope.subroot) all_issues.extend(issues) logger.debug( @@ -204,15 +219,20 @@ async def aforward( return all_issues def forward( - self, scopes: Sequence[ScopeResult], repo_path: Path + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, ) -> list[Issue]: """Analyze scopes for documentation issues (sync wrapper). Args: scopes: List of identified scopes with their changed files repo_path: Path to the cloned repository + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run Returns: List of documentation issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index 1b57f99..ec22eae 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -180,3 +180,42 @@ def restore_repo_paths(issues: list[Issue], subroot: str) -> None: for issue in issues: if issue.filename and not issue.filename.startswith(prefix): issue.filename = prefix + issue.filename + + +def issues_to_markdown(issues: list[Issue]) -> str: + """Format a list of issues as a compact Markdown report. + + Intended as an ``Episode`` artifact (see ``Hippocampus.aend_episode``) + so a scope's episode carries a human-readable snapshot of what the + module found for that call, alongside the consolidated context map. + + Args: + issues: Issues found for a given scope/call. + + Returns: + Markdown text. If ``issues`` is empty, a short "no issues" note. + """ + if not issues: + return "No issues found." + + lines = [f"## Issues ({len(issues)})", ""] + for issue in issues: + lines.extend([ + f"### {issue.title}", + "", + f"**Location:** `{issue.location}`", + f"**Category:** {issue.category.value}", + f"**Severity:** {issue.severity.value}", + "", + issue.description, + "", + ]) + if issue.suggestion: + lines.extend(["**Suggestion:**", issue.suggestion, ""]) + if issue.cwe_id: + lines.append(f"**Reference:** {issue.cwe_id}") + lines.append("") + lines.append("---") + lines.append("") + return "\n".join(lines) + diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 76b217f..70c05ae 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -48,6 +48,41 @@ class ScopeAssignment(BaseModel): reason: str = Field(description="Explanation for why this scope was identified") +def scope_assignments_to_markdown(assignments: list[ScopeAssignment]) -> str: + """Format scope assignments as a compact Markdown report. + + Intended as an ``Episode`` artifact (see ``Hippocampus.aend_episode``) so + the repo-level episode carries a human-readable snapshot of the scopes + identified for this call, alongside the consolidated context map. + + Args: + assignments: Scope assignments produced by the agent for this call. + + Returns: + Markdown text. If ``assignments`` is empty, a short "no scopes" note. + """ + if not assignments: + return "No scopes identified." + + lines = [f"## Scopes ({len(assignments)})", ""] + for assignment in assignments: + lines.extend([ + f"### {assignment.subroot}", + "", + f"**Type:** {assignment.scope_type.value}", + f"**Confidence:** {assignment.confidence}", + f"**Has changes:** {assignment.has_changes}", + f"**Is dependency:** {assignment.is_dependency}", + f"**Files:** {len(assignment.changed_files)}", + "", + assignment.reason, + "", + "---", + "", + ]) + return "\n".join(lines) + + class ScopeIdentifierSignature(dspy.Signature): """Identify code scopes in a repository for a merge request. @@ -196,13 +231,21 @@ async def _create_mcp_tools(self, repo_path: Path, is_local: bool = False) -> tu tools.extend(await connect_mcp_server(tools_dir / "git" / "server.py", [], contexts, caller)) return tools, contexts - async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = False) -> list[ScopeResult]: + async def aforward( + self, + mr: MergeRequest, + repo_path: Path, + is_local: bool = False, + run_id: str | None = None, + ) -> list[ScopeResult]: """Identify scopes in the repository for the given MR. Args: mr: The merge request to analyze repo_path: Path to the repository root is_local: If True, repo is already on disk (skip cloning) + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run (see ``Hippocampus.run_id``) """ # Get excluded directories from settings excluded_dirs = self._settings.excluded_directories @@ -268,6 +311,7 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal # title alone is the question, so no inputs get serialized. question_field="mr_title", task_name="scope", + run_id=run_id, ) result = await mem.aforward( changed_files=changed_file_paths, @@ -281,7 +325,13 @@ async def aforward(self, mr: MergeRequest, repo_path: Path, is_local: bool = Fal ) # Repo-level episode: subroot "." (no scope object exists yet). dir_path = f"/{repo}/root/" - await mem.aend_episode(get_memory_store(self._settings), dir_path) + await mem.aend_episode( + get_memory_store(self._settings), + dir_path, + artifacts={ + "scopes": scope_assignments_to_markdown(result.scopes) + }, + ) else: result = await agent.acall( changed_files=changed_file_paths, @@ -361,6 +411,12 @@ def _convert_assignments_to_results( )) return results - def forward(self, mr: MergeRequest, repo_path: Path, is_local: bool = False) -> list[ScopeResult]: + def forward( + self, + mr: MergeRequest, + repo_path: Path, + is_local: bool = False, + run_id: str | None = None, + ) -> list[ScopeResult]: """Identify scopes (sync wrapper).""" - return asyncio.run(self.aforward(mr, repo_path, is_local=is_local)) + return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id)) diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 454ee97..81c7d0c 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -10,7 +10,13 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult -from codespy.agents.reviewer.modules.helpers import MIN_CONFIDENCE, resolve_scope_root, strip_prefix, restore_repo_paths +from codespy.agents.reviewer.modules.helpers import ( + MIN_CONFIDENCE, + issues_to_markdown, + resolve_scope_root, + restore_repo_paths, + strip_prefix, +) from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server @@ -229,7 +235,12 @@ async def _create_osv_tools(self) -> tuple[list[Any], list[Any]]: return tools, contexts - async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list[Issue]: + async def aforward( + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, + ) -> list[Issue]: """Analyze scopes for supply chain security vulnerabilities and return issues. For each scope, filesystem/parser tools are created rooted at @@ -240,6 +251,8 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list Args: scopes: The scopes containing changed files to analyze repo_path: Path to the cloned repository for reading manifest files + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run (see ``Hippocampus.run_id``) Returns: List of security issues found across all scopes @@ -316,6 +329,7 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list "supply_chain" ), task_name="supply_chain", + run_id=run_id, ) result = await mem.aforward( manifest_path=manifest_path, @@ -323,8 +337,14 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list package_manager=package_manager, category=IssueCategory.SECURITY, ) + issues = [ + issue for issue in result.issues + if issue.confidence >= MIN_CONFIDENCE + ] await mem.aend_episode( - get_memory_store(self._settings), scope.scope_path() + get_memory_store(self._settings), + scope.scope_path(), + artifacts={"review": issues_to_markdown(issues)}, ) else: result = await supply_chain_agent.acall( @@ -333,10 +353,10 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list package_manager=package_manager, category=IssueCategory.SECURITY, ) - issues = [ - issue for issue in result.issues - if issue.confidence >= MIN_CONFIDENCE - ] + issues = [ + issue for issue in result.issues + if issue.confidence >= MIN_CONFIDENCE + ] # Restore repo-root-relative paths in reported issues restore_repo_paths(issues, scope.subroot) all_issues.extend(issues) @@ -351,14 +371,21 @@ async def aforward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list logger.info(f"Security audit found {len(all_issues)} issues") return all_issues - def forward(self, scopes: Sequence[ScopeResult], repo_path: Path) -> list[Issue]: + def forward( + self, + scopes: Sequence[ScopeResult], + repo_path: Path, + run_id: str | None = None, + ) -> list[Issue]: """Analyze scopes for supply chain security vulnerabilities (sync wrapper). Args: scopes: The scopes containing changed files to analyze repo_path: Path to the cloned repository for reading manifest files + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run Returns: List of security issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path)) \ No newline at end of file + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 50f69cd..2d96856 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -2,12 +2,15 @@ import asyncio import logging +import uuid from pathlib import Path import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, configure_dspy, get_cost_tracker, verify_model_access +from codespy.agents.memory.hippocampus import Hippocampus from codespy.config import Settings, get_settings +from codespy.config_memory import get_memory_store from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff from codespy.agents.reviewer.models import ( @@ -114,6 +117,7 @@ async def _run_review_modules( scopes: list, repo_path: Path, module_names: list[str], + run_id: str | None = None, ) -> list[Issue]: """Run review modules concurrently in a single event loop. @@ -125,14 +129,16 @@ async def _run_review_modules( scopes: Identified scopes with changed files repo_path: Path to the cloned repository module_names: Names of modules (for error logging) + run_id: Identifier of the pipeline run, shared across all agents + invoked within the same review run Returns: Aggregated list of issues from all modules """ tasks = [ - self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path), - self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path), - self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path), + self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), + self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), + self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), ] results = await asyncio.gather(*tasks, return_exceptions=True) @@ -170,7 +176,11 @@ def forward(self, config: ReviewConfig) -> ReviewResult: ReviewResult with issues, summary, costs, etc. """ self.cost_tracker.reset() - + + # Generate a single run_id shared across all agents/modules invoked + # within this pipeline run, used to correlate Episode records. + run_id = uuid.uuid4().hex + # Always verify model access self._verify_model_access() @@ -192,7 +202,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Identify scopes (the module internally checks if signature is enabled) is_local = isinstance(config, LocalReviewConfig) logger.info("Identifying code scopes...") - scopes = self.scope_identifier(mr, repo_path, is_local=is_local) + scopes = self.scope_identifier(mr, repo_path, is_local=is_local, run_id=run_id) for scope in scopes: logger.info(f" Scope: {scope.subroot} ({scope.scope_type.value}) - {len(scope.changed_files)} files") if scope.package_manifest: @@ -207,7 +217,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") all_issues = asyncio.run( - self._run_review_modules(scopes, repo_path, module_names) + self._run_review_modules(scopes, repo_path, module_names, run_id=run_id) ) logger.info(f"Found {len(all_issues)} issues") @@ -224,12 +234,42 @@ def forward(self, config: ReviewConfig) -> ReviewResult: summarizer = dspy.ChainOfThought(MRSummarySignature) # Track the summarization signature's costs with SignatureContext("summarization", self.cost_tracker): - result = summarizer( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - changed_files=scoped_files, - all_issues=all_issues, - ) + if self.settings.get_memory_enabled("summarization"): + mem = Hippocampus( + summarizer, + budget=self.settings.get_memory_budget("summarization"), + max_reflects=self.settings.get_memory_max_reflects( + "summarization" + ), + # ChainOfThought exposes no .signature, so the episode + # identity must be given explicitly. + task_name="summarization", + run_id=run_id, + ) + result = mem.forward( + mr_title=mr.title, + mr_description=mr.body or "No description provided.", + changed_files=scoped_files, + all_issues=all_issues, + ) + summary_md = ( + f"## Summary\n\n{result.summary}\n\n" + f"## Quality Assessment\n\n{result.quality_assessment}\n\n" + f"## Recommendation\n\n{result.recommendation}\n" + ) + # Repo-level episode: same "root" path used by scope_identifier. + mem.end_episode( + get_memory_store(self.settings), + f"/{mr.repo_slug}/root/", + artifacts={"summary": summary_md}, + ) + else: + result = summarizer( + mr_title=mr.title, + mr_description=mr.body or "No description provided.", + changed_files=scoped_files, + all_issues=all_issues, + ) summary = result.summary quality_assessment = result.quality_assessment recommendation = result.recommendation @@ -251,6 +291,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: mr_title=mr.title, mr_url=mr.url, repo=mr.repo_full_name, + run_id=run_id, model_used=self.settings.default_model, issues=all_issues, overall_summary=summary, From ee6fe454c87a8ede1f51382b447709e581017744 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 6 Aug 2026 23:45:41 +0200 Subject: [PATCH 25/79] wip --- ...plit-summarization-hippocampus-question.md | 512 ++++++++++++++++++ ...508-task-specific-hippocampus-questions.md | 173 ++++++ .../agents/memory/hippocampus/budget.py | 4 +- .../agents/memory/hippocampus/episode.py | 2 +- .../agents/memory/hippocampus/hippocampus.py | 12 +- src/codespy/agents/reviewer/models.py | 16 +- .../agents/reviewer/modules/__init__.py | 4 + .../agents/reviewer/modules/auditor.py | 121 +++++ .../agents/reviewer/modules/code_reviewer.py | 16 +- .../agents/reviewer/modules/doc_reviewer.py | 18 +- .../reviewer/modules/scope_identifier.py | 17 +- .../agents/reviewer/modules/summarizer.py | 101 ++++ .../reviewer/modules/supply_chain_auditor.py | 13 +- src/codespy/agents/reviewer/reviewer.py | 150 ++--- src/codespy/agents/reviewer/server.py | 2 +- src/codespy/config.py | 2 +- src/codespy/config_dspy.py | 3 +- src/codespy/config_memory.py | 2 +- 18 files changed, 1031 insertions(+), 137 deletions(-) create mode 100644 .kilo/plans/1785918176167-split-summarization-hippocampus-question.md create mode 100644 .kilo/plans/1786317978508-task-specific-hippocampus-questions.md create mode 100644 src/codespy/agents/reviewer/modules/auditor.py create mode 100644 src/codespy/agents/reviewer/modules/summarizer.py diff --git a/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md b/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md new file mode 100644 index 0000000..01a98cc --- /dev/null +++ b/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md @@ -0,0 +1,512 @@ +# Plan: Split Summarization & Replace question_field with question + +## Goal + +1. Split `MRSummarySignature` into **Summarizer** (runs before scope identification) and **Auditor** (runs after reviews). +2. Replace `Hippocampus.question_field` with a `question: str | None` parameter that directly accepts a pre-computed string. +3. Each module constructs a task-specific question string for its Hippocampus episodes. + +## Pipeline Flow (Before → After) + +**Before:** +``` +Fetch MR → Scope ID → Reviews (code, doc, supply_chain) → Summarization (summary + assessment + recommendation) +``` + +**After:** +``` +Fetch MR → Summary → Scope ID → Reviews (code, doc, supply_chain) → Audit (assessment + recommendation) +``` + +## Question Formats Per Module + +| Module | Question Template | +|--------|------------------| +| Summary | `"summarize {repo_slug}: pull request {mr_number} {mr_title}"` | +| Scope | `"identify scopes of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | +| Code Review | `"review code change of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Doc | `"review documentation of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Supply Chain | `"review supply chain of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Audit | `"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | + +**Data availability:** +- `repo_slug`: available from `mr.repo_slug` (orchestrator) or `scope.repo` (per-scope modules) +- `mr_number`: from `mr.number` — must be passed to per-scope modules +- `mr_title`: from `mr.title` — must be passed to per-scope modules +- `scope.subroot`: available inside per-scope iteration loops +- `summary`: produced by Summarizer, passed downstream as `pr_summary` + +--- + +## Tasks + +### 1. Hippocampus: Replace `question_field` with `question` + +**File:** `src/codespy/agents/memory/hippocampus/hippocampus.py` + +- Remove `question_field: str | None = None` from `__init__()` (line 110) +- Add `question: str | None = None` in its place +- Store as `self.question = question` (replaces `self.question_field` at line 169) +- Update `_make_question()` (lines 413-416): + ```python + def _make_question(self, inputs: dict) -> str: + if self.question is not None: + return self.question + return format_inputs(inputs, self.budget.max_question_tokens) + ``` +- Update docstrings referencing `question_field` throughout the file + +### 2. Update docstrings referencing `question_field` elsewhere + +**Files:** +- `src/codespy/agents/memory/hippocampus/budget.py` — lines 54, 58 +- `src/codespy/agents/memory/hippocampus/episode.py` — line 28 +- `src/codespy/config_memory.py` — line 106 +- `src/codespy/config.py` — line 310 + +Replace references to `question_field` with `question`. + +### 3. Create Summarizer module + +**New file:** `src/codespy/agents/reviewer/modules/summarizer.py` + +Follows the same pattern as existing modules (owns its signature, Hippocampus wrapping, config access): + +```python +"""PR summarizer module — produces a concise summary before scope identification.""" + +import logging + +import dspy + +from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.memory.hippocampus import Hippocampus +from codespy.config import get_settings +from codespy.config_memory import get_memory_store + +logger = logging.getLogger(__name__) + + +class PRSummarySignature(dspy.Signature): + """Summarize what a merge request does in 2-3 sentences. + + You are a busy Principal Engineer. Be extremely terse. State facts only. + Based on the title, description, and changed file paths, describe + what this MR accomplishes. No polite filler. No conversational language. + """ + + mr_title: str = dspy.InputField(desc="Title of the merge request") + mr_description: str = dspy.InputField(desc="Description/body of the MR") + changed_file_paths: list[str] = dspy.InputField( + desc="List of changed file paths from the MR" + ) + + summary: str = dspy.OutputField( + desc="2-3 sentence summary of what this MR accomplishes" + ) + + +class Summarizer(dspy.Module): + """Produces a concise PR summary used as Hippocampus question for all downstream modules.""" + + def __init__(self) -> None: + super().__init__() + self._cost_tracker = get_cost_tracker() + self._settings = get_settings() + + def forward( + self, + mr_title: str, + mr_description: str, + mr_number: int, + changed_file_paths: list[str], + repo_slug: str, + run_id: str | None = None, + ) -> str: + """Generate a PR summary. + + Args: + mr_title: Title of the merge request + mr_description: Description/body of the MR + mr_number: MR/PR number + changed_file_paths: List of changed file paths + repo_slug: Host-qualified repo slug for episode path + run_id: Pipeline run identifier + + Returns: + The summary string (2-3 sentences) + """ + if not self._settings.is_signature_enabled("summary"): + logger.debug("Skipping summary: disabled") + return mr_title or "No title" + + summarizer = dspy.ChainOfThought(PRSummarySignature) + logger.info("Generating PR summary...") + + question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" + + with SignatureContext("summary", self._cost_tracker): + if self._settings.get_memory_enabled("summary"): + mem = Hippocampus( + summarizer, + budget=self._settings.get_memory_budget("summary"), + max_reflects=self._settings.get_memory_max_reflects("summary"), + question=question, + task_name="summary", + run_id=run_id, + ) + result = mem.forward( + mr_title=mr_title, + mr_description=mr_description, + changed_file_paths=changed_file_paths, + ) + mem.end_episode( + get_memory_store(self._settings), + f"/{repo_slug}/root/", + artifacts={"summary": result.summary}, + ) + else: + result = summarizer( + mr_title=mr_title, + mr_description=mr_description, + changed_file_paths=changed_file_paths, + ) + + logger.info(f"PR summary: {result.summary[:80]}...") + return result.summary +``` + +### 4. Create Auditor module + +**New file:** `src/codespy/agents/reviewer/modules/auditor.py` + +```python +"""Auditor module — assesses code quality and provides recommendation after reviews.""" + +import logging +from typing import Sequence + +import dspy + +from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.memory.hippocampus import Hippocampus +from codespy.agents.reviewer.models import Issue +from codespy.config import get_settings +from codespy.config_memory import get_memory_store +from codespy.tools.git.models import ChangedFile + +logger = logging.getLogger(__name__) + + +class AuditSignature(dspy.Signature): + """Assess code quality and provide a recommendation for a merge request. + + You are a busy Principal Engineer. Be extremely terse. State facts only. + Based on the summary, changed files, and issues found during review, provide: + - An overall assessment of the code quality + - A recommendation (approve, request changes, or needs discussion) + + No polite filler. No conversational language. + """ + + mr_title: str = dspy.InputField(desc="Title of the merge request") + summary: str = dspy.InputField(desc="Summary of what this MR accomplishes") + changed_files: list[ChangedFile] = dspy.InputField( + desc="In-scope reviewable files with status and line counts" + ) + all_issues: list[Issue] = dspy.InputField( + desc="All issues found during review" + ) + + quality_assessment: str = dspy.OutputField( + desc="Overall assessment of code quality" + ) + recommendation: str = dspy.OutputField( + desc="One of: APPROVE, REQUEST_CHANGES, or NEEDS_DISCUSSION with brief justification" + ) + + +class Auditor(dspy.Module): + """Assesses code quality and recommends action after all reviews complete.""" + + def __init__(self) -> None: + super().__init__() + self._cost_tracker = get_cost_tracker() + self._settings = get_settings() + + def forward( + self, + mr_title: str, + mr_number: int, + pr_summary: str, + changed_files: Sequence[ChangedFile], + all_issues: Sequence[Issue], + repo_slug: str, + run_id: str | None = None, + ) -> tuple[str, str]: + """Assess quality and recommend action. + + Args: + mr_title: Title of the merge request + mr_number: MR/PR number + pr_summary: Summary produced by the Summarizer + changed_files: In-scope reviewable files + all_issues: All issues found during review + repo_slug: Host-qualified repo slug for episode path + run_id: Pipeline run identifier + + Returns: + Tuple of (quality_assessment, recommendation) + """ + if not self._settings.is_signature_enabled("audit"): + logger.debug("Skipping audit: disabled") + return ( + "Audit disabled.", + "NEEDS_DISCUSSION" if all_issues else "APPROVE", + ) + + auditor = dspy.ChainOfThought(AuditSignature) + logger.info("Running audit...") + + question = f"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {pr_summary}" + + with SignatureContext("audit", self._cost_tracker): + if self._settings.get_memory_enabled("audit"): + mem = Hippocampus( + auditor, + budget=self._settings.get_memory_budget("audit"), + max_reflects=self._settings.get_memory_max_reflects("audit"), + question=question, + task_name="audit", + run_id=run_id, + ) + result = mem.forward( + mr_title=mr_title, + summary=pr_summary, + changed_files=list(changed_files), + all_issues=list(all_issues), + ) + mem.end_episode( + get_memory_store(self._settings), + f"/{repo_slug}/root/", + artifacts={ + "audit": ( + f"## Quality Assessment\n\n{result.quality_assessment}\n\n" + f"## Recommendation\n\n{result.recommendation}\n" + ) + }, + ) + else: + result = auditor( + mr_title=mr_title, + summary=pr_summary, + changed_files=list(changed_files), + all_issues=list(all_issues), + ) + + return result.quality_assessment, result.recommendation +``` + +### 5. Export new modules + +**File:** `src/codespy/agents/reviewer/modules/__init__.py` + +Add `Summarizer` and `Auditor` to imports and `__all__`. + +### 6. Remove `MRSummarySignature` and update `ReviewPipeline` + +**File:** `src/codespy/agents/reviewer/reviewer.py` + +- Delete the `MRSummarySignature` class (lines 34-65) +- Remove its related imports (no longer needs `Hippocampus`, `get_memory_store` in this file) +- Add imports: `from codespy.agents.reviewer.modules import Summarizer, Auditor` +- Add `self.summarizer = Summarizer()` and `self.auditor = Auditor()` in `__init__()` +- Restructure `forward()`: + +```python +# After fetching/building MR, BEFORE scope identification: + +# 1. Run Summarizer +changed_file_paths = [f.filename for f in mr.changed_files] +pr_summary = self.summarizer( + mr_title=mr.title, + mr_description=mr.body or "No description provided.", + mr_number=mr.number, + changed_file_paths=changed_file_paths, + repo_slug=mr.repo_slug, + run_id=run_id, +) + +# 2. Scope identification (pass pr_summary) +scopes = self.scope_identifier(mr, repo_path, is_local=is_local, run_id=run_id, pr_summary=pr_summary) + +# 3. Reviews (pass pr_summary, mr_number, mr_title) +all_issues = asyncio.run( + self._run_review_modules( + scopes, repo_path, module_names, + run_id=run_id, pr_summary=pr_summary, + mr_number=mr.number, mr_title=mr.title, + ) +) + +# 4. Audit +scoped_files = self._collect_scoped_files(scopes) +quality_assessment, recommendation = self.auditor( + mr_title=mr.title, + mr_number=mr.number, + pr_summary=pr_summary, + changed_files=scoped_files, + all_issues=all_issues, + repo_slug=mr.repo_slug, + run_id=run_id, +) + +# Build ReviewResult with overall_summary=pr_summary, quality_assessment, recommendation +``` + +- Remove the entire old summarization block (lines 231-285) + +### 7. Update `_run_review_modules` + +**File:** `src/codespy/agents/reviewer/reviewer.py` + +Add `pr_summary: str`, `mr_number: int`, `mr_title: str` parameters and pass to each module: + +```python +async def _run_review_modules( + self, ..., pr_summary: str, mr_number: int, mr_title: str +) -> list[Issue]: + tasks = [ + self.code_reviewer.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, + ), + self.doc_reviewer.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, + ), + self.supply_chain_auditor.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, + ), + ] + ... +``` + +### 8. Update Scope Identifier + +**File:** `src/codespy/agents/reviewer/modules/scope_identifier.py` + +- Add `pr_summary: str | None = None` to `aforward()` and `forward()` signatures +- Construct question inside the memory-enabled block: + ```python + question = f"identify scopes of {mr.repo_slug}: pull request {mr.number} {mr.title}: {pr_summary}" + mem = Hippocampus( + agent, + budget=self._settings.get_memory_budget("scope"), + max_reflects=self._settings.get_memory_max_reflects("scope"), + question=question, + task_name="scope", + run_id=run_id, + ) + ``` +- Remove the old `question_field="mr_title"` and its comments (lines 310-312) + +### 9. Update Code Reviewer + +**File:** `src/codespy/agents/reviewer/modules/code_reviewer.py` + +- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures +- Construct per-scope question inside the scope loop: + ```python + question = ( + f"review code change of {scope.repo}: {scope.subroot}: " + f"pull request {mr_number} {mr_title}: {pr_summary}" + ) if pr_summary else None + mem = Hippocampus( + agent, + budget=self._settings.get_memory_budget("code_review"), + max_reflects=self._settings.get_memory_max_reflects("code_review"), + question=question, + task_name="code_review", + run_id=run_id, + ) + ``` +- Remove comment about "No question_field" (lines 239-241) + +### 10. Update Doc Reviewer + +**File:** `src/codespy/agents/reviewer/modules/doc_reviewer.py` + +- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures +- Construct per-scope question: + ```python + question = ( + f"review documentation of {scope.repo}: {scope.subroot}: " + f"pull request {mr_number} {mr_title}: {pr_summary}" + ) if pr_summary else None + ``` +- Pass `question=question` to `Hippocampus(...)` call +- Remove comment about "No question_field" (lines 173-175) + +### 11. Update Supply Chain Auditor + +**File:** `src/codespy/agents/reviewer/modules/supply_chain_auditor.py` + +- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures +- Construct per-scope question: + ```python + question = ( + f"review supply chain of {scope.repo}: {scope.subroot}: " + f"pull request {mr_number} {mr_title}: {pr_summary}" + ) if pr_summary else None + ``` +- Pass `question=question` to `Hippocampus(...)` call + +### 12. Update Config: Signature Names + +**File:** `src/codespy/config_dspy.py` + +Replace `"summarization"` with `"summary"` and `"audit"` in `SIGNATURE_NAMES`: +```python +SIGNATURE_NAMES = { + "code_review", + "doc", + "scope", + "supply_chain", + "summary", + "audit", +} +``` + +### 13. Update config references to "summarization" + +**File:** `src/codespy/agents/reviewer/models.py` — line 123 description mentions `summarization` + +**File:** `src/codespy/agents/reviewer/server.py` — line 107 mentions `summarization` + +Update these doc references to say `summary` and `audit`. + +--- + +## Edge Cases & Notes + +- **Memory disabled for Summary**: pipeline still works — `Summarizer.forward()` runs `ChainOfThought` directly, `pr_summary` is still produced. +- **Summary signature disabled**: `Summarizer.forward()` returns `mr.title` as fallback. +- **Audit signature disabled**: `Auditor.forward()` returns fallback strings. +- **`question=None` fallback**: When `pr_summary` is None (standalone usage outside pipeline), `Hippocampus._make_question()` falls back to `format_inputs()` bounded by `max_question_tokens`. +- **Per-scope questions**: Each Hippocampus instance within the scope loop gets a unique question containing `scope.subroot`, so episodes are identifiable per scope. +- **`scope.repo`**: Already equals `mr.repo_slug` (set in `scope_identifier._convert_assignments_to_results`), so per-scope modules use `scope.repo` directly. +- **Episode question field on `Episode` model**: stores the task-specific question string. +- **`ReviewResult` model unchanged**: `overall_summary` populated from `Summarizer`; `quality_assessment` and `recommendation` from `Auditor`. + +## Validation + +1. Run the full pipeline on a sample MR and verify: + - Summary runs before scope identification + - Each module's episode contains its task-specific question (grep episode JSON files) + - Questions are compact and identifiable (no huge serialized inputs) + - Audit produces quality_assessment + recommendation + - `ReviewResult` output structure unchanged +2. Verify env var overrides work for `"summary"` and `"audit"` (e.g. `SUMMARY_ENABLED=false`, `AUDIT_MODEL=...`) +3. Check episode file sizes are smaller (the original motivation) diff --git a/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md b/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md new file mode 100644 index 0000000..cb0e3d3 --- /dev/null +++ b/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md @@ -0,0 +1,173 @@ +# Plan: Task-Specific Hippocampus Question Formats + +## Goal + +Replace generic `question=pr_summary` / `question=mr_title` with structured, task-specific question strings per module so episodes are identifiable and semantically meaningful. Introduce `PRContext` dataclass to bundle the shared PR identity fields. + +## Target Question Formats + +| Module | Question | +|--------|----------| +| Summary | `"summarize {repo_slug}: pull request {mr_number} {mr_title}"` | +| Scope | `"identify scopes of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | +| Code Review | `"review code change of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Doc | `"review documentation of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Supply Chain | `"review supply chain of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | +| Audit | `"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | + +--- + +## Tasks + +### 1. Create `PRContext` dataclass + +**File:** `src/codespy/agents/reviewer/models.py` + +```python +class PRContext(BaseModel): + """Shared PR identity passed to all review modules after summarization. + + Built by the pipeline orchestrator after the Summarizer runs, then + threaded through scope identification, review modules, and audit. + Each module constructs its own Hippocampus question from these fields. + """ + + repo_slug: str = Field(description="Host-qualified repo identifier (e.g. github.com/owner/repo)") + mr_number: int = Field(description="MR/PR number") + mr_title: str = Field(description="MR/PR title") + summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") +``` + +### 2. Summarizer: add `mr_number`, format question + +**File:** `src/codespy/agents/reviewer/modules/summarizer.py` + +Summarizer is the producer of `summary` — it does NOT receive `PRContext`. + +- Add `mr_number: int` parameter to `forward()` (between `mr_description` and `changed_file_paths`) +- Replace `question=mr_title` with: + ```python + question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" + ``` + +### 3. Scope Identifier: accept `PRContext`, format question + +**File:** `src/codespy/agents/reviewer/modules/scope_identifier.py` + +- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` +- Construct question: + ```python + question = ( + f"identify scopes of {pr_context.repo_slug}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None + ``` + +### 4. Code Reviewer: accept `PRContext`, format per-scope question + +**File:** `src/codespy/agents/reviewer/modules/code_reviewer.py` + +- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` +- Construct per-scope question inside the scope loop: + ```python + question = ( + f"review code change of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None + ``` + +### 5. Doc Reviewer: accept `PRContext`, format per-scope question + +**File:** `src/codespy/agents/reviewer/modules/doc_reviewer.py` + +- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` +- Construct per-scope question: + ```python + question = ( + f"review documentation of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None + ``` + +### 6. Supply Chain Auditor: accept `PRContext`, format per-scope question + +**File:** `src/codespy/agents/reviewer/modules/supply_chain_auditor.py` + +- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` +- Construct per-scope question: + ```python + question = ( + f"review supply chain of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None + ``` + +### 7. Auditor: accept `PRContext`, format question + +**File:** `src/codespy/agents/reviewer/modules/auditor.py` + +- Replace `mr_title: str` and `pr_summary: str` params with `pr_context: PRContext` on `forward()` +- Remove `repo_slug: str` param (now from `pr_context.repo_slug`) +- Construct question: + ```python + question = ( + f"final audit of {pr_context.repo_slug}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) + ``` +- Update signature call to extract fields: + ```python + result = auditor/mem.forward( + mr_title=pr_context.mr_title, + summary=pr_context.summary, + changed_files=..., + all_issues=..., + ) + ``` +- Update episode path: `f"/{pr_context.repo_slug}/root/"` + +### 8. `_run_review_modules`: replace `pr_summary` with `pr_context` + +**File:** `src/codespy/agents/reviewer/reviewer.py` + +- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` +- Pass `pr_context=pr_context` to each module call (replacing `pr_summary=pr_summary`) + +### 9. `ReviewPipeline.forward()`: build `PRContext`, pass it downstream + +**File:** `src/codespy/agents/reviewer/reviewer.py` + +- Import `PRContext` from models +- Add `mr_number=mr.number` to `self.summarizer(...)` call +- After summarizer runs, build PRContext: + ```python + pr_context = PRContext( + repo_slug=mr.repo_slug, + mr_number=mr.number, + mr_title=mr.title, + summary=pr_summary, + ) + ``` +- Pass `pr_context=pr_context` to scope_identifier, `_run_review_modules`, and auditor +- Auditor call simplifies to: + ```python + quality_assessment, recommendation = self.auditor( + pr_context=pr_context, + changed_files=scoped_files, + all_issues=all_issues, + run_id=run_id, + ) + ``` + +### 10. Update `modules/__init__.py` export + +**File:** `src/codespy/agents/reviewer/modules/__init__.py` + +No change needed — `PRContext` lives in `models.py`, not modules. + +--- + +## Validation + +- Run the pipeline on a sample MR. Grep episode JSON `question` fields — each should match the specified format. +- Standalone module usage (without `pr_context`) still works: `if pr_context else None` guard falls back to `Hippocampus._make_question()` using `format_inputs()`. diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index ced9c46..48c10e1 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -51,11 +51,11 @@ class MemoryBudget: preserves both setup and conclusions. max_question_tokens: Budget for the serialized inputs used as the reflection "question", on the fallback path when - ``Hippocampus.question_field`` is None. None = unbounded, which is + ``Hippocampus.question`` is None. None = unbounded, which is rarely safe: *every* input field is serialized, so an agent taking a large field (a document dump, or a diff of every changed file) sends all of it to both the Distiller and the Cartographer. - Ignored when ``question_field`` is set — prefer that when a single + Ignored when ``question`` is set — prefer that when a single field cleanly captures intent. """ diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 5001abc..d08ad78 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -49,7 +49,7 @@ class Episode(BaseModel): ) task: str = Field(description="Wrapped signature name (or module class name as fallback)") module: str = Field(description="Wrapped dspy.Module class name") - question: str = Field(description="Question/task description for this episode") + question: str = Field(description="Question/task description for this episode (passed as 'question' or derived from serialized inputs)") context_map: ContextMap = Field(description="Consolidated context map snapshot") timestamp: datetime = Field( default_factory=lambda: datetime.now(UTC), diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index b1356ae..e6dba47 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -107,7 +107,7 @@ def __init__( module: dspy.Module, budget: MemoryBudget | None = None, max_reflects: int | None = None, - question_field: str | None = None, + question: str | None = None, task_name: str | None = None, run_id: str | None = None, ): @@ -124,8 +124,8 @@ class for per-field guidance. Resolve one from configuration with N: reflect online for the first N calls, buffer-only afterwards. Every call is always buffered regardless of this setting, so end_episode() is always available. - question_field: Name of the input field carrying the task description. - If set, only that field is used as the Distiller "question". + question: Pre-computed question string for the reflection "question". + If set, this string is used directly as the Distiller question. If None, all input fields are serialized (bounded by ``budget.max_question_tokens``). Set this when one field cleanly captures user intent. @@ -166,7 +166,7 @@ class for per-field guidance. Resolve one from configuration with self.cartograph = Cartographer() self.budget = budget or MemoryBudget() self.max_reflects = max_reflects - self.question_field = question_field + self.question = question self.cmap = ContextMap() self.scores: dict[str, int] = {} # Buffer of per-call bounded trajectory strings, cleared after end_episode(). @@ -411,8 +411,8 @@ def reset_episode(self) -> None: # ------------------------------------------------------------------ def _make_question(self, inputs: dict) -> str: - if self.question_field is not None: - return str(inputs.get(self.question_field, "")) + if self.question is not None: + return self.question return format_inputs(inputs, self.budget.max_question_tokens) def _distill(self, trajectory: str, question: str) -> None: diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index b3a33db..53b4564 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -7,6 +7,20 @@ from pydantic import BaseModel, Field, field_validator +class PRContext(BaseModel): + """Shared PR identity passed to all review modules after summarization. + + Built by the pipeline orchestrator after the Summarizer runs, then + threaded through scope identification, review modules, and audit. + Each module constructs its own Hippocampus question from these fields. + """ + + repo_slug: str = Field(description="Host-qualified repo identifier (e.g. github.com/owner/repo)") + mr_number: int = Field(description="MR/PR number") + mr_title: str = Field(description="MR/PR title") + summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") + + class IssueSeverity(str, Enum): """Severity level of an issue.""" @@ -120,7 +134,7 @@ def location(self) -> str: class SignatureStatsResult(BaseModel): """Statistics for a single signature's execution during review.""" - name: str = Field(description="Signature name (e.g., code_review, doc, scope, supply_chain, summarization)") + name: str = Field(description="Signature name (e.g., code_review, doc, scope, supply_chain, summary, audit)") cost: float = Field(default=0.0, description="Cost in USD for this signature") tokens: int = Field(default=0, description="Tokens used by this signature") call_count: int = Field(default=0, description="Number of LLM calls made by this signature") diff --git a/src/codespy/agents/reviewer/modules/__init__.py b/src/codespy/agents/reviewer/modules/__init__.py index 6c98666..549d770 100644 --- a/src/codespy/agents/reviewer/modules/__init__.py +++ b/src/codespy/agents/reviewer/modules/__init__.py @@ -1,13 +1,17 @@ """DSPy modules for code review.""" +from codespy.agents.reviewer.modules.auditor import Auditor from codespy.agents.reviewer.modules.code_reviewer import CodeReviewer from codespy.agents.reviewer.modules.doc_reviewer import DocReviewer from codespy.agents.reviewer.modules.scope_identifier import ScopeIdentifier +from codespy.agents.reviewer.modules.summarizer import Summarizer from codespy.agents.reviewer.modules.supply_chain_auditor import SupplyChainAuditor __all__ = [ + "Auditor", "CodeReviewer", "DocReviewer", "ScopeIdentifier", + "Summarizer", "SupplyChainAuditor", ] diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py new file mode 100644 index 0000000..fc76f86 --- /dev/null +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -0,0 +1,121 @@ +"""Auditor module — assesses code quality and provides recommendation after reviews.""" + +import logging +from typing import Sequence + +import dspy + +from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.memory.hippocampus import Hippocampus +from codespy.agents.reviewer.models import Issue, PRContext +from codespy.config import get_settings +from codespy.config_memory import get_memory_store +from codespy.tools.git.models import ChangedFile + +logger = logging.getLogger(__name__) + + +class AuditSignature(dspy.Signature): + """Assess code quality and provide a recommendation for a merge request. + + You are a busy Principal Engineer. Be extremely terse. State facts only. + Based on the summary, changed files, and issues found during review, provide: + - An overall assessment of the code quality + - A recommendation (approve, request changes, or needs discussion) + + No polite filler. No conversational language. + """ + + mr_title: str = dspy.InputField(desc="Title of the merge request") + summary: str = dspy.InputField(desc="Summary of what this MR accomplishes") + changed_files: list[ChangedFile] = dspy.InputField( + desc="In-scope reviewable files with status and line counts" + ) + all_issues: list[Issue] = dspy.InputField( + desc="All issues found during review" + ) + + quality_assessment: str = dspy.OutputField( + desc="Overall assessment of code quality" + ) + recommendation: str = dspy.OutputField( + desc="One of: APPROVE, REQUEST_CHANGES, or NEEDS_DISCUSSION with brief justification" + ) + + +class Auditor(dspy.Module): + """Assesses code quality and recommends action after all reviews complete.""" + + def __init__(self) -> None: + super().__init__() + self._cost_tracker = get_cost_tracker() + self._settings = get_settings() + + def forward( + self, + pr_context: PRContext, + changed_files: Sequence[ChangedFile], + all_issues: Sequence[Issue], + run_id: str | None = None, + ) -> tuple[str, str]: + """Assess quality and recommend action. + + Args: + pr_context: PR context containing repo_slug, mr_number, mr_title, summary + changed_files: In-scope reviewable files + all_issues: All issues found during review + run_id: Pipeline run identifier + + Returns: + Tuple of (quality_assessment, recommendation) + """ + if not self._settings.is_signature_enabled("audit"): + logger.debug("Skipping audit: disabled") + return ( + "Audit disabled.", + "NEEDS_DISCUSSION" if all_issues else "APPROVE", + ) + + auditor = dspy.ChainOfThought(AuditSignature) + logger.info("Running audit...") + + question = ( + f"final audit of {pr_context.repo_slug}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) + + with SignatureContext("audit", self._cost_tracker): + if self._settings.get_memory_enabled("audit"): + mem = Hippocampus( + auditor, + budget=self._settings.get_memory_budget("audit"), + max_reflects=self._settings.get_memory_max_reflects("audit"), + question=question, + task_name="audit", + run_id=run_id, + ) + result = mem( + mr_title=pr_context.mr_title, + summary=pr_context.summary, + changed_files=list(changed_files), + all_issues=list(all_issues), + ) + mem.end_episode( + get_memory_store(self._settings), + f"/{pr_context.repo_slug}/root/", + artifacts={ + "audit": ( + f"## Quality Assessment\n\n{result.quality_assessment}\n\n" + f"## Recommendation\n\n{result.recommendation}\n" + ) + }, + ) + else: + result = auditor( + mr_title=pr_context.mr_title, + summary=pr_context.summary, + changed_files=list(changed_files), + all_issues=list(all_issues), + ) + + return result.quality_assessment, result.recommendation diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 6d47547..0f865a4 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -9,7 +9,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult +from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -184,6 +184,7 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for defects, security issues, and code smells. @@ -192,6 +193,7 @@ async def aforward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) + pr_context: PR context used to construct Hippocampus question per scope Returns: List of bug, security, and smell issues found across all scopes @@ -236,13 +238,15 @@ async def aforward( ) async with SignatureContext("code_review", self._cost_tracker): if self._settings.get_memory_enabled("code_review"): - # No question_field: the scope (with every patch) is the - # only input, so budget.max_question_tokens must bound - # the serialized question. + question = ( + f"review code change of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None mem = Hippocampus( agent, budget=self._settings.get_memory_budget("code_review"), max_reflects=self._settings.get_memory_max_reflects("code_review"), + question=question, task_name="code_review", run_id=run_id, ) @@ -286,6 +290,7 @@ def forward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for code issues (sync wrapper). @@ -294,8 +299,9 @@ def forward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run + pr_context: PR context used to construct Hippocampus question per scope Returns: List of bug, security, and smell issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 639e47d..988a9ef 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -9,7 +9,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult +from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -117,6 +117,7 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for documentation issues. @@ -125,6 +126,7 @@ async def aforward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) + pr_context: PR context used to construct Hippocampus question per scope Returns: List of documentation issues found across all scopes @@ -170,15 +172,15 @@ async def aforward( ) async with SignatureContext("doc", self._cost_tracker): if self._settings.get_memory_enabled("doc"): - # No question_field: patches + documentation are both - # large, so budget.max_question_tokens must bound the - # serialized question. + question = ( + f"review documentation of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None mem = Hippocampus( reviewer, budget=self._settings.get_memory_budget("doc"), max_reflects=self._settings.get_memory_max_reflects("doc"), - # ChainOfThought exposes no .signature, so the episode - # identity must be given explicitly. + question=question, task_name="doc", run_id=run_id, ) @@ -223,6 +225,7 @@ def forward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for documentation issues (sync wrapper). @@ -231,8 +234,9 @@ def forward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run + pr_context: PR context used to construct Hippocampus question per scope Returns: List of documentation issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 70c05ae..33820a0 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -10,7 +10,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType +from codespy.agents.reviewer.models import PackageManifest, PRContext, ScopeResult, ScopeType from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file @@ -237,15 +237,17 @@ async def aforward( repo_path: Path, is_local: bool = False, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[ScopeResult]: """Identify scopes in the repository for the given MR. - + Args: mr: The merge request to analyze repo_path: Path to the repository root is_local: If True, repo is already on disk (skip cloning) run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) + pr_context: PR context used to construct Hippocampus question """ # Get excluded directories from settings excluded_dirs = self._settings.excluded_directories @@ -303,13 +305,15 @@ async def aforward( # Track scope signature costs async with SignatureContext("scope", self._cost_tracker): if self._settings.get_memory_enabled("scope"): + question = ( + f"identify scopes of {pr_context.repo_slug}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None mem = Hippocampus( agent, budget=self._settings.get_memory_budget("scope"), max_reflects=self._settings.get_memory_max_reflects("scope"), - # question_field makes budget.max_question_tokens moot: the - # title alone is the question, so no inputs get serialized. - question_field="mr_title", + question=question, task_name="scope", run_id=run_id, ) @@ -417,6 +421,7 @@ def forward( repo_path: Path, is_local: bool = False, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[ScopeResult]: """Identify scopes (sync wrapper).""" - return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id)) + return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id, pr_context=pr_context)) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py new file mode 100644 index 0000000..c745547 --- /dev/null +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -0,0 +1,101 @@ +"""PR summarizer module — produces a concise summary before scope identification.""" + +import logging + +import dspy + +from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.memory.hippocampus import Hippocampus +from codespy.config import get_settings +from codespy.config_memory import get_memory_store + +logger = logging.getLogger(__name__) + + +class PRSummarySignature(dspy.Signature): + """Summarize what a merge request does in 2-3 sentences. + + You are a busy Principal Engineer. Be extremely terse. State facts only. + Based on the title, description, and changed file paths, describe + what this MR accomplishes. No polite filler. No conversational language. + """ + + mr_title: str = dspy.InputField(desc="Title of the merge request") + mr_description: str = dspy.InputField(desc="Description/body of the MR") + changed_file_paths: list[str] = dspy.InputField( + desc="List of changed file paths from the MR" + ) + + summary: str = dspy.OutputField( + desc="2-3 sentence summary of what this MR accomplishes" + ) + + +class Summarizer(dspy.Module): + """Produces a concise PR summary used as Hippocampus question for all downstream modules.""" + + def __init__(self) -> None: + super().__init__() + self._cost_tracker = get_cost_tracker() + self._settings = get_settings() + + def forward( + self, + mr_title: str, + mr_description: str, + mr_number: int, + changed_file_paths: list[str], + repo_slug: str, + run_id: str | None = None, + ) -> str: + """Generate a PR summary. + + Args: + mr_title: Title of the merge request + mr_description: Description/body of the MR + mr_number: MR/PR number + changed_file_paths: List of changed file paths + repo_slug: Host-qualified repo slug for episode path + run_id: Pipeline run identifier + + Returns: + The summary string (2-3 sentences) + """ + if not self._settings.is_signature_enabled("summary"): + logger.debug("Skipping summary: disabled") + return mr_title or "No title" + + summarizer = dspy.ChainOfThought(PRSummarySignature) + logger.info("Generating PR summary...") + + question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" + + with SignatureContext("summary", self._cost_tracker): + if self._settings.get_memory_enabled("summary"): + mem = Hippocampus( + summarizer, + budget=self._settings.get_memory_budget("summary"), + max_reflects=self._settings.get_memory_max_reflects("summary"), + question=question, + task_name="summary", + run_id=run_id, + ) + result = mem( + mr_title=mr_title, + mr_description=mr_description, + changed_file_paths=changed_file_paths, + ) + mem.end_episode( + get_memory_store(self._settings), + f"/{repo_slug}/root/", + artifacts={"summary": result.summary}, + ) + else: + result = summarizer( + mr_title=mr_title, + mr_description=mr_description, + changed_file_paths=changed_file_paths, + ) + + logger.info(f"PR summary: {result.summary[:80]}...") + return result.summary diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 81c7d0c..9a264f6 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -9,7 +9,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, ScopeResult +from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -240,6 +240,7 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for supply chain security vulnerabilities and return issues. @@ -253,6 +254,7 @@ async def aforward( repo_path: Path to the cloned repository for reading manifest files run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) + pr_context: PR context used to construct Hippocampus question per scope Returns: List of security issues found across all scopes @@ -322,12 +324,17 @@ async def aforward( # Track supply_chain signature costs separately async with SignatureContext("supply_chain", self._cost_tracker): if self._settings.get_memory_enabled("supply_chain"): + question = ( + f"review supply chain of {scope.repo}: {scope.subroot}: " + f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + ) if pr_context else None mem = Hippocampus( supply_chain_agent, budget=self._settings.get_memory_budget("supply_chain"), max_reflects=self._settings.get_memory_max_reflects( "supply_chain" ), + question=question, task_name="supply_chain", run_id=run_id, ) @@ -376,6 +383,7 @@ def forward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Analyze scopes for supply chain security vulnerabilities (sync wrapper). @@ -384,8 +392,9 @@ def forward( repo_path: Path to the cloned repository for reading manifest files run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run + pr_context: PR context used to construct Hippocampus question per scope Returns: List of security issues found across all scopes """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 2d96856..3606826 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -4,17 +4,17 @@ import logging import uuid from pathlib import Path +from typing import Sequence import dspy # type: ignore[import-untyped] -from codespy.agents import SignatureContext, configure_dspy, get_cost_tracker, verify_model_access -from codespy.agents.memory.hippocampus import Hippocampus +from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access from codespy.config import Settings, get_settings -from codespy.config_memory import get_memory_store from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff from codespy.agents.reviewer.models import ( Issue, + PRContext, SignatureStatsResult, ReviewResult, ReviewConfig, @@ -22,49 +22,17 @@ LocalReviewConfig, ) from codespy.agents.reviewer.modules import ( + Auditor, CodeReviewer, DocReviewer, ScopeIdentifier, + Summarizer, SupplyChainAuditor, ) logger = logging.getLogger(__name__) -class MRSummarySignature(dspy.Signature): - """Generate an overall summary and recommendation for a merge request. - - You are a busy Principal Engineer. Be extremely terse. State facts only. - - Based on all the issues found during review, provide: - - A concise summary of what the MR does - - An overall assessment of the code quality - - A recommendation (approve, request changes, or needs discussion) - - OUTPUT RULES: Be direct and terse. No polite filler ("I suggest", "Great job", "Well done"). - No conversational language. State facts and assessments only. - """ - - mr_title: str = dspy.InputField(desc="Title of the merge request") - mr_description: str = dspy.InputField(desc="Description/body of the MR") - changed_files: list[ChangedFile] = dspy.InputField( - desc="In-scope reviewable files (excludes binaries, vendor, lock files, etc.) with status and line counts" - ) - all_issues: list[Issue] = dspy.InputField( - desc="All issues found during review" - ) - - summary: str = dspy.OutputField( - desc="2-3 sentence summary of what this MR accomplishes" - ) - quality_assessment: str = dspy.OutputField( - desc="Overall assessment of code quality (e.g., well-structured, needs refactoring, follows best practices, etc.)" - ) - recommendation: str = dspy.OutputField( - desc="One of: APPROVE, REQUEST_CHANGES, or NEEDS_DISCUSSION with brief justification" - ) - - class ReviewPipeline(dspy.Module): """Orchestrates the code review process using DSPy modules.""" @@ -81,6 +49,8 @@ def __init__(self, settings: Settings | None = None) -> None: self.code_reviewer = CodeReviewer() self.doc_reviewer = DocReviewer() self.supply_chain_auditor = SupplyChainAuditor() + self.summarizer = Summarizer() + self.auditor = Auditor() def _verify_model_access(self) -> None: """Verify LLM model access.""" @@ -118,6 +88,7 @@ async def _run_review_modules( repo_path: Path, module_names: list[str], run_id: str | None = None, + pr_context: PRContext | None = None, ) -> list[Issue]: """Run review modules concurrently in a single event loop. @@ -131,14 +102,15 @@ async def _run_review_modules( module_names: Names of modules (for error logging) run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run + pr_context: PR context for Hippocampus question (passed to all modules) Returns: Aggregated list of issues from all modules """ tasks = [ - self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), - self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), - self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id), + self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), + self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), + self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), ] results = await asyncio.gather(*tasks, return_exceptions=True) @@ -199,10 +171,31 @@ def forward(self, config: ReviewConfig) -> ReviewResult: else: raise ValueError(f"Invalid config type: {type(config)}") - # Identify scopes (the module internally checks if signature is enabled) + # Step 1: Run Summarizer (before scope identification) + changed_file_paths = [f.filename for f in mr.changed_files] + pr_summary = self.summarizer( + mr_title=mr.title, + mr_description=mr.body or "No description provided.", + mr_number=mr.number, + changed_file_paths=changed_file_paths, + repo_slug=mr.repo_slug, + run_id=run_id, + ) + + # Build PRContext after summarizer runs + pr_context = PRContext( + repo_slug=mr.repo_slug, + mr_number=mr.number, + mr_title=mr.title, + summary=pr_summary, + ) + + # Step 2: Identify scopes (pass pr_context) is_local = isinstance(config, LocalReviewConfig) logger.info("Identifying code scopes...") - scopes = self.scope_identifier(mr, repo_path, is_local=is_local, run_id=run_id) + scopes = self.scope_identifier( + mr, repo_path, is_local=is_local, run_id=run_id, pr_context=pr_context + ) for scope in scopes: logger.info(f" Scope: {scope.subroot} ({scope.scope_type.value}) - {len(scope.changed_files)} files") if scope.package_manifest: @@ -213,76 +206,27 @@ def forward(self, config: ReviewConfig) -> ReviewResult: if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") - # Run review modules concurrently via asyncio.gather + # Step 3: Run review modules concurrently via asyncio.gather (pass pr_context) module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") all_issues = asyncio.run( - self._run_review_modules(scopes, repo_path, module_names, run_id=run_id) + self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, pr_context=pr_context) ) logger.info(f"Found {len(all_issues)} issues") - # Collect in-scope files from identified scopes (excludes binaries, vendor, lock files, etc.) + # Step 4: Run Audit scoped_files = self._collect_scoped_files(scopes) logger.info( - f"Summary input: {len(scoped_files)} in-scope files " + f"Audit input: {len(scoped_files)} in-scope files " f"(filtered from {len(mr.changed_files)} total)" ) - # Generate summary, quality assessment, and recommendation - if self.settings.is_signature_enabled("summarization"): - logger.info("Generating MR summary...") - try: - summarizer = dspy.ChainOfThought(MRSummarySignature) - # Track the summarization signature's costs - with SignatureContext("summarization", self.cost_tracker): - if self.settings.get_memory_enabled("summarization"): - mem = Hippocampus( - summarizer, - budget=self.settings.get_memory_budget("summarization"), - max_reflects=self.settings.get_memory_max_reflects( - "summarization" - ), - # ChainOfThought exposes no .signature, so the episode - # identity must be given explicitly. - task_name="summarization", - run_id=run_id, - ) - result = mem.forward( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - changed_files=scoped_files, - all_issues=all_issues, - ) - summary_md = ( - f"## Summary\n\n{result.summary}\n\n" - f"## Quality Assessment\n\n{result.quality_assessment}\n\n" - f"## Recommendation\n\n{result.recommendation}\n" - ) - # Repo-level episode: same "root" path used by scope_identifier. - mem.end_episode( - get_memory_store(self.settings), - f"/{mr.repo_slug}/root/", - artifacts={"summary": summary_md}, - ) - else: - result = summarizer( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - changed_files=scoped_files, - all_issues=all_issues, - ) - summary = result.summary - quality_assessment = result.quality_assessment - recommendation = result.recommendation - except Exception as e: - logger.error(f"Failed to generate summary: {e}") - summary = f"Reviewed {len(scoped_files)} files with {len(all_issues)} issues." - quality_assessment = "Unable to assess due to error." - recommendation = "NEEDS_DISCUSSION: Summary generation failed." - else: - logger.debug("Skipping summarization: disabled") - summary = f"Reviewed {len(scoped_files)} files with {len(all_issues)} issues." - quality_assessment = "Summarization disabled." - recommendation = "NEEDS_DISCUSSION" if all_issues else "APPROVE" + quality_assessment, recommendation = self.auditor( + pr_context=pr_context, + changed_files=scoped_files, + all_issues=all_issues, + run_id=run_id, + ) + # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() @@ -294,7 +238,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: run_id=run_id, model_used=self.settings.default_model, issues=all_issues, - overall_summary=summary, + overall_summary=pr_summary, quality_assessment=quality_assessment, recommendation=recommendation, total_cost=self.cost_tracker.total_cost, diff --git a/src/codespy/agents/reviewer/server.py b/src/codespy/agents/reviewer/server.py index b77f60d..e38ce98 100644 --- a/src/codespy/agents/reviewer/server.py +++ b/src/codespy/agents/reviewer/server.py @@ -104,7 +104,7 @@ async def review_local_changes( No PR or remote platform required — works with any local git repository. Diffs the current HEAD against the base_ref to find changed files, then runs the full codespy review pipeline (scope identification, code & doc review, - supply chain audit, and summarization). + supply chain audit, summary, and audit). Args: repo_path: Absolute path to the local git repository to review diff --git a/src/codespy/config.py b/src/codespy/config.py index 891f4c8..43edc59 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -307,7 +307,7 @@ def get_memory_max_question_tokens(self, signature_name: str) -> int | None: """Get max_question_tokens for a signature's memory (signature-specific or default). Bounds the serialized agent inputs used as the reflection "question". - Ignored when the caller passes an explicit ``question_field``. + Ignored when the caller passes an explicit ``question`` string. """ config = self.get_signature_config(signature_name).memory return ( diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index d1a267f..674a506 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -52,7 +52,8 @@ class SignatureConfig(BaseModel): "doc", "scope", "supply_chain", - "summarization", + "summary", + "audit", } # Create uppercase prefixes for matching (e.g., "CODE_REVIEW_", "SUPPLY_CHAIN_") diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index dcbb215..a989839 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -103,7 +103,7 @@ class MemoryConfig(BaseModel): default_max_trajectory_tokens: int | None = 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS # Head+tail cap on the serialized agent inputs used as the Distiller/Cartographer - # "question". Only applies when the caller passes no question_field: otherwise + # "question". Only applies when the caller passes no 'question': otherwise # every input field is serialized, which for code review means the full patch # of every changed file. See Hippocampus.max_question_tokens. default_max_question_tokens: int | None = 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS From 7b3676ed7aa06a9c5e8a7490808c6cca092d1170 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 6 Aug 2026 23:53:39 +0200 Subject: [PATCH 26/79] wip --- .../agents/memory/hippocampus/episode.py | 10 ++++----- .../agents/memory/hippocampus/hippocampus.py | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index d08ad78..0064473 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -34,11 +34,11 @@ class Episode(BaseModel): this episode (e.g. ``{"review": ""}``). Agent-agnostic: any module can attach whatever markdown/text output it produced under a key of its choosing. Empty by default. - run_id: Identifier of the pipeline run that produced this episode. - Shared by every agent/module invoked within the same - ``ReviewPipeline.forward()`` call, so all episodes from one - review run can be correlated. Also used as the ```` suffix - in the episode filename: ``-.json``. + run_id: Identifier of the pipeline run that produced this episode. + Shared by every agent/module invoked within the same + ``ReviewPipeline.forward()`` call, so all episodes from one + review run can be correlated. Also used as the ```` prefix + in the episode filename: ``--.json``. """ run_id: str = Field( default="", diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index e6dba47..90cba42 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -189,6 +189,9 @@ class for per-field guidance. Resolve one from configuration with # above). Falls back to a random UUID for standalone usage where no # orchestrator provides one. self._run_id: str = run_id or uuid.uuid4().hex + # Counter for episode filenames to avoid collisions when the same + # signature is invoked multiple times on the same scope within one run. + self._episode_index: int = 0 # The most recent consolidated Episode; set by end_episode(), None until then. self.episode: Episode | None = None @@ -270,20 +273,23 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: self._reflected_count = 0 - def _episode_file_path(self, dir: str) -> str: + def _episode_file_path(self, dir: str, index: int = 0) -> str: """Build the full episode file path from a directory. Prepends the ``episodes`` root and appends a hidden ``.codespy`` folder holding the episode file, named after the pipeline run's - identifier and the wrapped task: - ``global/episodic//.codespy/-.json``. + identifier, the wrapped task, and an optional index to avoid collisions: + ``global/episodic//.codespy/--.json``. Args: dir: Directory identifying where this episode belongs (e.g. a scope's ``/{repo}/{subroot}/`` path). + index: Episode index for this scope/task combination. Used to + disambiguate when the same signature is invoked multiple + times on the same scope within a single pipeline run. """ trimmed = dir.strip("/") - return f"global/episodic/{trimmed}/.codespy/{self._run_id}-{self._task_name}.json" + return f"global/episodic/{trimmed}/.codespy/{self._run_id}-{self._task_name}-{index}.json" def end_episode( self, @@ -326,7 +332,8 @@ def end_episode( return self._finalize_episode(artifacts) if store is not None and dir is not None: - _save_episode(store, self._episode_file_path(dir), self.episode) + _save_episode(store, self._episode_file_path(dir, self._episode_index), self.episode) + self._episode_index += 1 async def aend_episode( self, @@ -354,8 +361,9 @@ async def aend_episode( return await asyncio.to_thread(self._finalize_episode, artifacts) if store is not None and dir is not None: - path = self._episode_file_path(dir) + path = self._episode_file_path(dir, self._episode_index) await asyncio.to_thread(_save_episode, store, path, self.episode) + self._episode_index += 1 def save_episode(self, store: Storage, path: str) -> None: @@ -399,12 +407,14 @@ def load_episode(self, store: Storage, path: str) -> None: self._episode_trajectories.clear() self._episode_question = None self._reflected_count = 0 + self._episode_index = 0 def reset_episode(self) -> None: """Discard the buffered trajectories without reflecting.""" self._episode_trajectories.clear() self._episode_question = None self._reflected_count = 0 + self._episode_index = 0 # ------------------------------------------------------------------ # Internals From f034219f7c4b0296f6c449f6eb1f294614440664 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 6 Aug 2026 23:56:03 +0200 Subject: [PATCH 27/79] remove .kilo --- ...plit-summarization-hippocampus-question.md | 512 ------------------ ...508-task-specific-hippocampus-questions.md | 173 ------ 2 files changed, 685 deletions(-) delete mode 100644 .kilo/plans/1785918176167-split-summarization-hippocampus-question.md delete mode 100644 .kilo/plans/1786317978508-task-specific-hippocampus-questions.md diff --git a/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md b/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md deleted file mode 100644 index 01a98cc..0000000 --- a/.kilo/plans/1785918176167-split-summarization-hippocampus-question.md +++ /dev/null @@ -1,512 +0,0 @@ -# Plan: Split Summarization & Replace question_field with question - -## Goal - -1. Split `MRSummarySignature` into **Summarizer** (runs before scope identification) and **Auditor** (runs after reviews). -2. Replace `Hippocampus.question_field` with a `question: str | None` parameter that directly accepts a pre-computed string. -3. Each module constructs a task-specific question string for its Hippocampus episodes. - -## Pipeline Flow (Before → After) - -**Before:** -``` -Fetch MR → Scope ID → Reviews (code, doc, supply_chain) → Summarization (summary + assessment + recommendation) -``` - -**After:** -``` -Fetch MR → Summary → Scope ID → Reviews (code, doc, supply_chain) → Audit (assessment + recommendation) -``` - -## Question Formats Per Module - -| Module | Question Template | -|--------|------------------| -| Summary | `"summarize {repo_slug}: pull request {mr_number} {mr_title}"` | -| Scope | `"identify scopes of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | -| Code Review | `"review code change of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Doc | `"review documentation of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Supply Chain | `"review supply chain of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Audit | `"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | - -**Data availability:** -- `repo_slug`: available from `mr.repo_slug` (orchestrator) or `scope.repo` (per-scope modules) -- `mr_number`: from `mr.number` — must be passed to per-scope modules -- `mr_title`: from `mr.title` — must be passed to per-scope modules -- `scope.subroot`: available inside per-scope iteration loops -- `summary`: produced by Summarizer, passed downstream as `pr_summary` - ---- - -## Tasks - -### 1. Hippocampus: Replace `question_field` with `question` - -**File:** `src/codespy/agents/memory/hippocampus/hippocampus.py` - -- Remove `question_field: str | None = None` from `__init__()` (line 110) -- Add `question: str | None = None` in its place -- Store as `self.question = question` (replaces `self.question_field` at line 169) -- Update `_make_question()` (lines 413-416): - ```python - def _make_question(self, inputs: dict) -> str: - if self.question is not None: - return self.question - return format_inputs(inputs, self.budget.max_question_tokens) - ``` -- Update docstrings referencing `question_field` throughout the file - -### 2. Update docstrings referencing `question_field` elsewhere - -**Files:** -- `src/codespy/agents/memory/hippocampus/budget.py` — lines 54, 58 -- `src/codespy/agents/memory/hippocampus/episode.py` — line 28 -- `src/codespy/config_memory.py` — line 106 -- `src/codespy/config.py` — line 310 - -Replace references to `question_field` with `question`. - -### 3. Create Summarizer module - -**New file:** `src/codespy/agents/reviewer/modules/summarizer.py` - -Follows the same pattern as existing modules (owns its signature, Hippocampus wrapping, config access): - -```python -"""PR summarizer module — produces a concise summary before scope identification.""" - -import logging - -import dspy - -from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.config import get_settings -from codespy.config_memory import get_memory_store - -logger = logging.getLogger(__name__) - - -class PRSummarySignature(dspy.Signature): - """Summarize what a merge request does in 2-3 sentences. - - You are a busy Principal Engineer. Be extremely terse. State facts only. - Based on the title, description, and changed file paths, describe - what this MR accomplishes. No polite filler. No conversational language. - """ - - mr_title: str = dspy.InputField(desc="Title of the merge request") - mr_description: str = dspy.InputField(desc="Description/body of the MR") - changed_file_paths: list[str] = dspy.InputField( - desc="List of changed file paths from the MR" - ) - - summary: str = dspy.OutputField( - desc="2-3 sentence summary of what this MR accomplishes" - ) - - -class Summarizer(dspy.Module): - """Produces a concise PR summary used as Hippocampus question for all downstream modules.""" - - def __init__(self) -> None: - super().__init__() - self._cost_tracker = get_cost_tracker() - self._settings = get_settings() - - def forward( - self, - mr_title: str, - mr_description: str, - mr_number: int, - changed_file_paths: list[str], - repo_slug: str, - run_id: str | None = None, - ) -> str: - """Generate a PR summary. - - Args: - mr_title: Title of the merge request - mr_description: Description/body of the MR - mr_number: MR/PR number - changed_file_paths: List of changed file paths - repo_slug: Host-qualified repo slug for episode path - run_id: Pipeline run identifier - - Returns: - The summary string (2-3 sentences) - """ - if not self._settings.is_signature_enabled("summary"): - logger.debug("Skipping summary: disabled") - return mr_title or "No title" - - summarizer = dspy.ChainOfThought(PRSummarySignature) - logger.info("Generating PR summary...") - - question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" - - with SignatureContext("summary", self._cost_tracker): - if self._settings.get_memory_enabled("summary"): - mem = Hippocampus( - summarizer, - budget=self._settings.get_memory_budget("summary"), - max_reflects=self._settings.get_memory_max_reflects("summary"), - question=question, - task_name="summary", - run_id=run_id, - ) - result = mem.forward( - mr_title=mr_title, - mr_description=mr_description, - changed_file_paths=changed_file_paths, - ) - mem.end_episode( - get_memory_store(self._settings), - f"/{repo_slug}/root/", - artifacts={"summary": result.summary}, - ) - else: - result = summarizer( - mr_title=mr_title, - mr_description=mr_description, - changed_file_paths=changed_file_paths, - ) - - logger.info(f"PR summary: {result.summary[:80]}...") - return result.summary -``` - -### 4. Create Auditor module - -**New file:** `src/codespy/agents/reviewer/modules/auditor.py` - -```python -"""Auditor module — assesses code quality and provides recommendation after reviews.""" - -import logging -from typing import Sequence - -import dspy - -from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue -from codespy.config import get_settings -from codespy.config_memory import get_memory_store -from codespy.tools.git.models import ChangedFile - -logger = logging.getLogger(__name__) - - -class AuditSignature(dspy.Signature): - """Assess code quality and provide a recommendation for a merge request. - - You are a busy Principal Engineer. Be extremely terse. State facts only. - Based on the summary, changed files, and issues found during review, provide: - - An overall assessment of the code quality - - A recommendation (approve, request changes, or needs discussion) - - No polite filler. No conversational language. - """ - - mr_title: str = dspy.InputField(desc="Title of the merge request") - summary: str = dspy.InputField(desc="Summary of what this MR accomplishes") - changed_files: list[ChangedFile] = dspy.InputField( - desc="In-scope reviewable files with status and line counts" - ) - all_issues: list[Issue] = dspy.InputField( - desc="All issues found during review" - ) - - quality_assessment: str = dspy.OutputField( - desc="Overall assessment of code quality" - ) - recommendation: str = dspy.OutputField( - desc="One of: APPROVE, REQUEST_CHANGES, or NEEDS_DISCUSSION with brief justification" - ) - - -class Auditor(dspy.Module): - """Assesses code quality and recommends action after all reviews complete.""" - - def __init__(self) -> None: - super().__init__() - self._cost_tracker = get_cost_tracker() - self._settings = get_settings() - - def forward( - self, - mr_title: str, - mr_number: int, - pr_summary: str, - changed_files: Sequence[ChangedFile], - all_issues: Sequence[Issue], - repo_slug: str, - run_id: str | None = None, - ) -> tuple[str, str]: - """Assess quality and recommend action. - - Args: - mr_title: Title of the merge request - mr_number: MR/PR number - pr_summary: Summary produced by the Summarizer - changed_files: In-scope reviewable files - all_issues: All issues found during review - repo_slug: Host-qualified repo slug for episode path - run_id: Pipeline run identifier - - Returns: - Tuple of (quality_assessment, recommendation) - """ - if not self._settings.is_signature_enabled("audit"): - logger.debug("Skipping audit: disabled") - return ( - "Audit disabled.", - "NEEDS_DISCUSSION" if all_issues else "APPROVE", - ) - - auditor = dspy.ChainOfThought(AuditSignature) - logger.info("Running audit...") - - question = f"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {pr_summary}" - - with SignatureContext("audit", self._cost_tracker): - if self._settings.get_memory_enabled("audit"): - mem = Hippocampus( - auditor, - budget=self._settings.get_memory_budget("audit"), - max_reflects=self._settings.get_memory_max_reflects("audit"), - question=question, - task_name="audit", - run_id=run_id, - ) - result = mem.forward( - mr_title=mr_title, - summary=pr_summary, - changed_files=list(changed_files), - all_issues=list(all_issues), - ) - mem.end_episode( - get_memory_store(self._settings), - f"/{repo_slug}/root/", - artifacts={ - "audit": ( - f"## Quality Assessment\n\n{result.quality_assessment}\n\n" - f"## Recommendation\n\n{result.recommendation}\n" - ) - }, - ) - else: - result = auditor( - mr_title=mr_title, - summary=pr_summary, - changed_files=list(changed_files), - all_issues=list(all_issues), - ) - - return result.quality_assessment, result.recommendation -``` - -### 5. Export new modules - -**File:** `src/codespy/agents/reviewer/modules/__init__.py` - -Add `Summarizer` and `Auditor` to imports and `__all__`. - -### 6. Remove `MRSummarySignature` and update `ReviewPipeline` - -**File:** `src/codespy/agents/reviewer/reviewer.py` - -- Delete the `MRSummarySignature` class (lines 34-65) -- Remove its related imports (no longer needs `Hippocampus`, `get_memory_store` in this file) -- Add imports: `from codespy.agents.reviewer.modules import Summarizer, Auditor` -- Add `self.summarizer = Summarizer()` and `self.auditor = Auditor()` in `__init__()` -- Restructure `forward()`: - -```python -# After fetching/building MR, BEFORE scope identification: - -# 1. Run Summarizer -changed_file_paths = [f.filename for f in mr.changed_files] -pr_summary = self.summarizer( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - mr_number=mr.number, - changed_file_paths=changed_file_paths, - repo_slug=mr.repo_slug, - run_id=run_id, -) - -# 2. Scope identification (pass pr_summary) -scopes = self.scope_identifier(mr, repo_path, is_local=is_local, run_id=run_id, pr_summary=pr_summary) - -# 3. Reviews (pass pr_summary, mr_number, mr_title) -all_issues = asyncio.run( - self._run_review_modules( - scopes, repo_path, module_names, - run_id=run_id, pr_summary=pr_summary, - mr_number=mr.number, mr_title=mr.title, - ) -) - -# 4. Audit -scoped_files = self._collect_scoped_files(scopes) -quality_assessment, recommendation = self.auditor( - mr_title=mr.title, - mr_number=mr.number, - pr_summary=pr_summary, - changed_files=scoped_files, - all_issues=all_issues, - repo_slug=mr.repo_slug, - run_id=run_id, -) - -# Build ReviewResult with overall_summary=pr_summary, quality_assessment, recommendation -``` - -- Remove the entire old summarization block (lines 231-285) - -### 7. Update `_run_review_modules` - -**File:** `src/codespy/agents/reviewer/reviewer.py` - -Add `pr_summary: str`, `mr_number: int`, `mr_title: str` parameters and pass to each module: - -```python -async def _run_review_modules( - self, ..., pr_summary: str, mr_number: int, mr_title: str -) -> list[Issue]: - tasks = [ - self.code_reviewer.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, - ), - self.doc_reviewer.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, - ), - self.supply_chain_auditor.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - pr_summary=pr_summary, mr_number=mr_number, mr_title=mr_title, - ), - ] - ... -``` - -### 8. Update Scope Identifier - -**File:** `src/codespy/agents/reviewer/modules/scope_identifier.py` - -- Add `pr_summary: str | None = None` to `aforward()` and `forward()` signatures -- Construct question inside the memory-enabled block: - ```python - question = f"identify scopes of {mr.repo_slug}: pull request {mr.number} {mr.title}: {pr_summary}" - mem = Hippocampus( - agent, - budget=self._settings.get_memory_budget("scope"), - max_reflects=self._settings.get_memory_max_reflects("scope"), - question=question, - task_name="scope", - run_id=run_id, - ) - ``` -- Remove the old `question_field="mr_title"` and its comments (lines 310-312) - -### 9. Update Code Reviewer - -**File:** `src/codespy/agents/reviewer/modules/code_reviewer.py` - -- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures -- Construct per-scope question inside the scope loop: - ```python - question = ( - f"review code change of {scope.repo}: {scope.subroot}: " - f"pull request {mr_number} {mr_title}: {pr_summary}" - ) if pr_summary else None - mem = Hippocampus( - agent, - budget=self._settings.get_memory_budget("code_review"), - max_reflects=self._settings.get_memory_max_reflects("code_review"), - question=question, - task_name="code_review", - run_id=run_id, - ) - ``` -- Remove comment about "No question_field" (lines 239-241) - -### 10. Update Doc Reviewer - -**File:** `src/codespy/agents/reviewer/modules/doc_reviewer.py` - -- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures -- Construct per-scope question: - ```python - question = ( - f"review documentation of {scope.repo}: {scope.subroot}: " - f"pull request {mr_number} {mr_title}: {pr_summary}" - ) if pr_summary else None - ``` -- Pass `question=question` to `Hippocampus(...)` call -- Remove comment about "No question_field" (lines 173-175) - -### 11. Update Supply Chain Auditor - -**File:** `src/codespy/agents/reviewer/modules/supply_chain_auditor.py` - -- Add `pr_summary: str | None = None`, `mr_number: int | None = None`, `mr_title: str | None = None` to `aforward()` and `forward()` signatures -- Construct per-scope question: - ```python - question = ( - f"review supply chain of {scope.repo}: {scope.subroot}: " - f"pull request {mr_number} {mr_title}: {pr_summary}" - ) if pr_summary else None - ``` -- Pass `question=question` to `Hippocampus(...)` call - -### 12. Update Config: Signature Names - -**File:** `src/codespy/config_dspy.py` - -Replace `"summarization"` with `"summary"` and `"audit"` in `SIGNATURE_NAMES`: -```python -SIGNATURE_NAMES = { - "code_review", - "doc", - "scope", - "supply_chain", - "summary", - "audit", -} -``` - -### 13. Update config references to "summarization" - -**File:** `src/codespy/agents/reviewer/models.py` — line 123 description mentions `summarization` - -**File:** `src/codespy/agents/reviewer/server.py` — line 107 mentions `summarization` - -Update these doc references to say `summary` and `audit`. - ---- - -## Edge Cases & Notes - -- **Memory disabled for Summary**: pipeline still works — `Summarizer.forward()` runs `ChainOfThought` directly, `pr_summary` is still produced. -- **Summary signature disabled**: `Summarizer.forward()` returns `mr.title` as fallback. -- **Audit signature disabled**: `Auditor.forward()` returns fallback strings. -- **`question=None` fallback**: When `pr_summary` is None (standalone usage outside pipeline), `Hippocampus._make_question()` falls back to `format_inputs()` bounded by `max_question_tokens`. -- **Per-scope questions**: Each Hippocampus instance within the scope loop gets a unique question containing `scope.subroot`, so episodes are identifiable per scope. -- **`scope.repo`**: Already equals `mr.repo_slug` (set in `scope_identifier._convert_assignments_to_results`), so per-scope modules use `scope.repo` directly. -- **Episode question field on `Episode` model**: stores the task-specific question string. -- **`ReviewResult` model unchanged**: `overall_summary` populated from `Summarizer`; `quality_assessment` and `recommendation` from `Auditor`. - -## Validation - -1. Run the full pipeline on a sample MR and verify: - - Summary runs before scope identification - - Each module's episode contains its task-specific question (grep episode JSON files) - - Questions are compact and identifiable (no huge serialized inputs) - - Audit produces quality_assessment + recommendation - - `ReviewResult` output structure unchanged -2. Verify env var overrides work for `"summary"` and `"audit"` (e.g. `SUMMARY_ENABLED=false`, `AUDIT_MODEL=...`) -3. Check episode file sizes are smaller (the original motivation) diff --git a/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md b/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md deleted file mode 100644 index cb0e3d3..0000000 --- a/.kilo/plans/1786317978508-task-specific-hippocampus-questions.md +++ /dev/null @@ -1,173 +0,0 @@ -# Plan: Task-Specific Hippocampus Question Formats - -## Goal - -Replace generic `question=pr_summary` / `question=mr_title` with structured, task-specific question strings per module so episodes are identifiable and semantically meaningful. Introduce `PRContext` dataclass to bundle the shared PR identity fields. - -## Target Question Formats - -| Module | Question | -|--------|----------| -| Summary | `"summarize {repo_slug}: pull request {mr_number} {mr_title}"` | -| Scope | `"identify scopes of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | -| Code Review | `"review code change of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Doc | `"review documentation of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Supply Chain | `"review supply chain of {repo_slug}: {scope.subroot}: pull request {mr_number} {mr_title}: {summary}"` | -| Audit | `"final audit of {repo_slug}: pull request {mr_number} {mr_title}: {summary}"` | - ---- - -## Tasks - -### 1. Create `PRContext` dataclass - -**File:** `src/codespy/agents/reviewer/models.py` - -```python -class PRContext(BaseModel): - """Shared PR identity passed to all review modules after summarization. - - Built by the pipeline orchestrator after the Summarizer runs, then - threaded through scope identification, review modules, and audit. - Each module constructs its own Hippocampus question from these fields. - """ - - repo_slug: str = Field(description="Host-qualified repo identifier (e.g. github.com/owner/repo)") - mr_number: int = Field(description="MR/PR number") - mr_title: str = Field(description="MR/PR title") - summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") -``` - -### 2. Summarizer: add `mr_number`, format question - -**File:** `src/codespy/agents/reviewer/modules/summarizer.py` - -Summarizer is the producer of `summary` — it does NOT receive `PRContext`. - -- Add `mr_number: int` parameter to `forward()` (between `mr_description` and `changed_file_paths`) -- Replace `question=mr_title` with: - ```python - question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" - ``` - -### 3. Scope Identifier: accept `PRContext`, format question - -**File:** `src/codespy/agents/reviewer/modules/scope_identifier.py` - -- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` -- Construct question: - ```python - question = ( - f"identify scopes of {pr_context.repo_slug}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None - ``` - -### 4. Code Reviewer: accept `PRContext`, format per-scope question - -**File:** `src/codespy/agents/reviewer/modules/code_reviewer.py` - -- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` -- Construct per-scope question inside the scope loop: - ```python - question = ( - f"review code change of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None - ``` - -### 5. Doc Reviewer: accept `PRContext`, format per-scope question - -**File:** `src/codespy/agents/reviewer/modules/doc_reviewer.py` - -- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` -- Construct per-scope question: - ```python - question = ( - f"review documentation of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None - ``` - -### 6. Supply Chain Auditor: accept `PRContext`, format per-scope question - -**File:** `src/codespy/agents/reviewer/modules/supply_chain_auditor.py` - -- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` on `aforward()` and `forward()` -- Construct per-scope question: - ```python - question = ( - f"review supply chain of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None - ``` - -### 7. Auditor: accept `PRContext`, format question - -**File:** `src/codespy/agents/reviewer/modules/auditor.py` - -- Replace `mr_title: str` and `pr_summary: str` params with `pr_context: PRContext` on `forward()` -- Remove `repo_slug: str` param (now from `pr_context.repo_slug`) -- Construct question: - ```python - question = ( - f"final audit of {pr_context.repo_slug}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) - ``` -- Update signature call to extract fields: - ```python - result = auditor/mem.forward( - mr_title=pr_context.mr_title, - summary=pr_context.summary, - changed_files=..., - all_issues=..., - ) - ``` -- Update episode path: `f"/{pr_context.repo_slug}/root/"` - -### 8. `_run_review_modules`: replace `pr_summary` with `pr_context` - -**File:** `src/codespy/agents/reviewer/reviewer.py` - -- Replace `pr_summary: str | None = None` param with `pr_context: PRContext | None = None` -- Pass `pr_context=pr_context` to each module call (replacing `pr_summary=pr_summary`) - -### 9. `ReviewPipeline.forward()`: build `PRContext`, pass it downstream - -**File:** `src/codespy/agents/reviewer/reviewer.py` - -- Import `PRContext` from models -- Add `mr_number=mr.number` to `self.summarizer(...)` call -- After summarizer runs, build PRContext: - ```python - pr_context = PRContext( - repo_slug=mr.repo_slug, - mr_number=mr.number, - mr_title=mr.title, - summary=pr_summary, - ) - ``` -- Pass `pr_context=pr_context` to scope_identifier, `_run_review_modules`, and auditor -- Auditor call simplifies to: - ```python - quality_assessment, recommendation = self.auditor( - pr_context=pr_context, - changed_files=scoped_files, - all_issues=all_issues, - run_id=run_id, - ) - ``` - -### 10. Update `modules/__init__.py` export - -**File:** `src/codespy/agents/reviewer/modules/__init__.py` - -No change needed — `PRContext` lives in `models.py`, not modules. - ---- - -## Validation - -- Run the pipeline on a sample MR. Grep episode JSON `question` fields — each should match the specified format. -- Standalone module usage (without `pr_context`) still works: `if pr_context else None` guard falls back to `Hippocampus._make_question()` using `format_inputs()`. From d0fe81ee1372072e85b0df803c198b1c44fd3eee Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 6 Aug 2026 23:56:20 +0200 Subject: [PATCH 28/79] remove .kilo --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d17395f..8549e48 100644 --- a/.gitignore +++ b/.gitignore @@ -92,4 +92,5 @@ dmypy.json Thumbs.db # Project specific -.cache/ \ No newline at end of file +.cache/ +.kilo/ \ No newline at end of file From 217e13f08e9fae48573802ab223e7cb53a26f21e Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 7 Aug 2026 00:15:21 +0200 Subject: [PATCH 29/79] wip --- README.md | 6 +-- action.yml | 44 +++++++++---------- codespy.yaml | 34 +++++++------- src/codespy/agents/reviewer/models.py | 9 ++-- .../agents/reviewer/modules/auditor.py | 2 +- .../reviewer/modules/scope_identifier.py | 2 +- .../agents/reviewer/modules/summarizer.py | 2 +- 7 files changed, 50 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2e3fbe1..1b2d56c 100644 --- a/README.md +++ b/README.md @@ -512,7 +512,7 @@ To optimize costs, override the mid-tier and cheap models: # .env or environment variables DEFAULT_MODEL=anthropic/claude-opus-4-6 # Smart tier (default) EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 # Mid-tier: field extraction -SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 # Cheap tier: PR summary +SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # Cheap tier: PR summary ``` Or in `codespy.yaml`: @@ -521,7 +521,7 @@ Or in `codespy.yaml`: default_model: anthropic/claude-opus-4-6 extraction_model: anthropic/claude-sonnet-4-5-20250929 signatures: - summarization: + summary: model: anthropic/claude-haiku-4-5-20251001 ``` @@ -685,7 +685,7 @@ The review is powered by DSPy signatures that structure the LLM's analysis: | **CodeReviewSignature** | `code_review` | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells | | **DocReviewSignature** | `doc` | Detects stale or wrong documentation caused by code changes | | **SupplyChainSecuritySignature** | `supply_chain` | Analyzes artifacts (Dockerfiles) and dependencies for supply chain security | -| **MRSummarySignature** | `summarization` | Generates summary, quality assessment, and recommendation | +| **MRSummarySignature** | `summary` | Generates summary, quality assessment, and recommendation | ## Supported Languages diff --git a/action.yml b/action.yml index 8f864ee..58a18d9 100644 --- a/action.yml +++ b/action.yml @@ -192,23 +192,23 @@ inputs: default: 'false' # ========================================== - # SIGNATURE: summarization + # SIGNATURE: summary # ========================================== - summarization-enabled: - description: 'Enable PR summarization signature' + summary-enabled: + description: 'Enable PR summary signature' required: false default: 'true' - - summarization-model: - description: 'Model for summarization (empty = use default)' + + summary-model: + description: 'Model for summary (empty = use default)' required: false - - summarization-reasoning-effort: - description: 'Reasoning effort for summarization (minimal|low|medium|high)' + + summary-reasoning-effort: + description: 'Reasoning effort for summary (minimal|low|medium|high)' required: false - - summarization-temperature: - description: 'Temperature for summarization' + + summary-temperature: + description: 'Temperature for summary' required: false excluded-directories: @@ -311,11 +311,11 @@ runs: SUPPLY_CHAIN_TEMPERATURE: ${{ inputs.supply-chain-temperature }} SUPPLY_CHAIN_SCAN_UNCHANGED: ${{ inputs.supply-chain-scan-unchanged }} - # Summarization signature - SUMMARIZATION_ENABLED: ${{ inputs.summarization-enabled }} - SUMMARIZATION_MODEL: ${{ inputs.summarization-model }} - SUMMARIZATION_REASONING_EFFORT: ${{ inputs.summarization-reasoning-effort }} - SUMMARIZATION_TEMPERATURE: ${{ inputs.summarization-temperature }} + # Summary signature + SUMMARY_ENABLED: ${{ inputs.summary-enabled }} + SUMMARY_MODEL: ${{ inputs.summary-model }} + SUMMARY_REASONING_EFFORT: ${{ inputs.summary-reasoning-effort }} + SUMMARY_TEMPERATURE: ${{ inputs.summary-temperature }} # Other settings EXCLUDED_DIRECTORIES: ${{ inputs.excluded-directories }} @@ -371,11 +371,11 @@ runs: [ -n "$SUPPLY_CHAIN_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_TEMPERATURE" [ -n "$SUPPLY_CHAIN_SCAN_UNCHANGED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_SCAN_UNCHANGED" - # Summarization - [ -n "$SUMMARIZATION_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_ENABLED" - [ -n "$SUMMARIZATION_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_MODEL" - [ -n "$SUMMARIZATION_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_REASONING_EFFORT" - [ -n "$SUMMARIZATION_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARIZATION_TEMPERATURE" + # Summary + [ -n "$SUMMARY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_ENABLED" + [ -n "$SUMMARY_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MODEL" + [ -n "$SUMMARY_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_REASONING_EFFORT" + [ -n "$SUMMARY_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_TEMPERATURE" # Other settings [ -n "$EXCLUDED_DIRECTORIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e EXCLUDED_DIRECTORIES" diff --git a/codespy.yaml b/codespy.yaml index 8516b27..b211dd3 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -61,9 +61,9 @@ gitlab: # MEMORY # ============================================================================ # Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# and the summarization step consolidate their run into a ContextMap and +# and the summary step consolidate their run into a ContextMap and # persist it as an Episode. Save-only for now (no loading). Disabled by -# default globally; enabled by default for summarization — see `memory:` +# default globally; enabled by default for summary — see `memory:` # blocks under each signature below. # @@ -146,7 +146,7 @@ memory: # extraction (TwoStepAdapter). Needs accuracy but not deep reasoning. # Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. # -# Cheap (summarization): Used for PR summary generation. Simple synthesis +# Cheap (summary): Used for PR summary generation. Simple synthesis # task. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # # Cheap (memory.distiller / memory.cartographer): Memory reflection — @@ -154,12 +154,12 @@ memory: # tasks. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # # By default, all models fall back to default_model. Override extraction_model, -# the summarization model, and the reflection models for cost optimization: +# the summary model, and the reflection models for cost optimization: # # default_model: anthropic/claude-opus-4-6 # extraction_model: anthropic/claude-sonnet-4-5-20250929 # signatures: -# summarization: +# summary: # model: anthropic/claude-haiku-4-5-20251001 # memory: # distiller: @@ -263,19 +263,19 @@ signatures: max_question_tokens: null # SCOPE_MEMORY_MAX_QUESTION_TOKENS # Summarizer signature - summarization: - enabled: true # SUMMARIZATION_ENABLED - model: null # SUMMARIZATION_MODEL (falls back to default_model) - reasoning_effort: null # SUMMARIZATION_REASONING_EFFORT - temperature: null # SUMMARIZATION_TEMPERATURE - max_tokens: null # SUMMARIZATION_MAX_TOKENS + summary: + enabled: true # SUMMARY_ENABLED + model: null # SUMMARY_MODEL (falls back to default_model) + reasoning_effort: null # SUMMARY_REASONING_EFFORT + temperature: null # SUMMARY_TEMPERATURE + max_tokens: null # SUMMARY_MAX_TOKENS memory: - enabled: true # SUMMARIZATION_MEMORY_ENABLED (null -> memory.default_enabled) - max_reflects: null # SUMMARIZATION_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS - max_context_item_tokens: null # SUMMARIZATION_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS + enabled: true # SUMMARY_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # SUMMARY_MEMORY_MAX_REFLECTS + max_context_map_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_item_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS + max_trajectory_tokens: null # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # SUMMARY_MEMORY_MAX_QUESTION_TOKENS # ============================================================================ # OUTPUT diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 53b4564..db486ed 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -94,13 +94,14 @@ class ScopeResult(BaseModel): model_config = {"arbitrary_types_allowed": True} def scope_path(self) -> str: - """Return the storage-relative path for this scope: ``/{repo}/{subroot}/``. + """Return the storage-relative path for this scope. Used by Hippocampus memory as the base directory for episode files. - ``subroot == "."`` (repo root) is normalized to ``"root"``. + ``subroot == "."`` (repo root) results in ``/{repo}/``. """ - subroot = "root" if self.subroot in (".", "") else self.subroot.strip("/") - return f"/{self.repo}/{subroot}/" + if self.subroot in (".", ""): + return f"/{self.repo}/" + return f"/{self.repo}/{self.subroot.strip('/')}/" class Issue(BaseModel): diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index fc76f86..3a10ceb 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -102,7 +102,7 @@ def forward( ) mem.end_episode( get_memory_store(self._settings), - f"/{pr_context.repo_slug}/root/", + f"/{pr_context.repo_slug}/", artifacts={ "audit": ( f"## Quality Assessment\n\n{result.quality_assessment}\n\n" diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 33820a0..7451194 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -328,7 +328,7 @@ async def aforward( is_local=is_local, ) # Repo-level episode: subroot "." (no scope object exists yet). - dir_path = f"/{repo}/root/" + dir_path = f"/{repo}/" await mem.aend_episode( get_memory_store(self._settings), dir_path, diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index c745547..6ded4ed 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -87,7 +87,7 @@ def forward( ) mem.end_episode( get_memory_store(self._settings), - f"/{repo_slug}/root/", + f"/{repo_slug}/", artifacts={"summary": result.summary}, ) else: From 9c7a6aa79b7f73554f2657118e91b58e2afd703b Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 7 Aug 2026 01:18:08 +0200 Subject: [PATCH 30/79] wip --- .../agents/memory/hippocampus/__init__.py | 2 + .../agents/memory/hippocampus/context_map.py | 27 +++++++ .../agents/memory/hippocampus/episode.py | 14 ++-- .../agents/memory/hippocampus/hippocampus.py | 79 ++++++++++++++++++- .../memory/hippocampus/modules/__init__.py | 2 + 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index 11aa664..7e4ce45 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -4,6 +4,7 @@ ContextMap, Item, ItemTag, + Mutation, Operation, OpType, SectionName, @@ -25,6 +26,7 @@ "Item", "ItemTag", "MemoryBudget", + "Mutation", "Operation", "OpType", "SectionName", diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py index 51abfea..c0f0ce5 100644 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ b/src/codespy/agents/memory/hippocampus/context_map.py @@ -69,6 +69,21 @@ class Operation(BaseModel): content: str | None = Field(default=None, description="Required for ADD / REPLACE.") +class Mutation(BaseModel): + """A recorded Cartographer mutation applied to the context map. + + Tracks the sequence of ADD/DELETE/REPLACE operations with pre-mutation + state for debugging and audit purposes. + """ + + step: int = Field(description="Which _distill() pass produced this mutation (0-indexed)") + type: OpType = Field(description="Type of mutation: ADD, DELETE, or REPLACE") + item_id: str = Field(description="Generated ID (ADD) or existing ID (DELETE/REPLACE)") + section: SectionName = Field(description="Section the item belongs to") + content: str | None = Field(default=None, description="New content (ADD/REPLACE); None for DELETE") + previous_content: str | None = Field(default=None, description="Old content (DELETE/REPLACE); None for ADD") + + class ContextMap(BaseModel): context_roadmap: list[Item] = Field( default_factory=list, @@ -107,6 +122,18 @@ def section_names(cls) -> list[str]: def section(self, name: str) -> list[Item]: return getattr(self, name) + def find_item(self, item_id: str) -> tuple[SectionName, Item] | None: + """Look up an item by ID across all sections. + + Returns: + Tuple of (section_name, item) if found, None otherwise. + """ + for sec in self.section_names(): + for it in self.section(sec): + if it.id == item_id: + return sec, it + return None + def all_items(self) -> list[Item]: return [it for s in self.section_names() for it in self.section(s)] diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 0064473..5744def 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field -from codespy.agents.memory.hippocampus.context_map import ContextMap +from codespy.agents.memory.hippocampus.context_map import ContextMap, Mutation from codespy.tools.storage.base import Storage @@ -47,18 +47,22 @@ class Episode(BaseModel): "Shared across all agents invoked within the same review run." ), ) - task: str = Field(description="Wrapped signature name (or module class name as fallback)") - module: str = Field(description="Wrapped dspy.Module class name") - question: str = Field(description="Question/task description for this episode (passed as 'question' or derived from serialized inputs)") - context_map: ContextMap = Field(description="Consolidated context map snapshot") timestamp: datetime = Field( default_factory=lambda: datetime.now(UTC), description="UTC time the episode was recorded", ) + task: str = Field(description="Wrapped signature name (or module class name as fallback)") + module: str = Field(description="Wrapped dspy.Module class name") + question: str = Field(description="Question/task description for this episode (passed as 'question' or derived from serialized inputs)") artifacts: dict[str, str] = Field( default_factory=dict, description="Named output artifacts produced by the agent (e.g. {'review': ''})", ) + context_map: ContextMap = Field(description="Consolidated context map snapshot") + mutations: list[Mutation] = Field( + default_factory=list, + description="Ordered sequence of Cartographer mutations applied during this episode", + ) def save_episode(store: Storage, path: str, episode: Episode) -> None: diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 90cba42..0418dfd 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -15,7 +15,7 @@ format_inputs, format_trajectory, ) -from codespy.agents.memory.hippocampus.context_map import ContextMap, ItemTag +from codespy.agents.memory.hippocampus.context_map import ContextMap, ItemTag, Mutation, Operation, OpType from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.hippocampus.episode import load_episode as _load_episode from codespy.agents.memory.hippocampus.episode import save_episode as _save_episode @@ -194,6 +194,10 @@ class for per-field guidance. Resolve one from configuration with self._episode_index: int = 0 # The most recent consolidated Episode; set by end_episode(), None until then. self.episode: Episode | None = None + # Accumulated mutations across _distill() calls within the current episode. + self._mutations: list[Mutation] = [] + # Step counter incremented per _distill() call for mutation grouping. + self._distill_step: int = 0 @property def current_map_text(self) -> str: @@ -267,10 +271,13 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: timestamp=datetime.now(UTC), artifacts=artifacts or {}, run_id=self._run_id, + mutations=self._mutations, ) self._episode_trajectories.clear() self._episode_question = None self._reflected_count = 0 + self._mutations.clear() + self._distill_step = 0 def _episode_file_path(self, dir: str, index: int = 0) -> str: @@ -408,6 +415,8 @@ def load_episode(self, store: Storage, path: str) -> None: self._episode_question = None self._reflected_count = 0 self._episode_index = 0 + self._mutations.clear() + self._distill_step = 0 def reset_episode(self) -> None: """Discard the buffered trajectories without reflecting.""" @@ -415,6 +424,8 @@ def reset_episode(self) -> None: self._episode_question = None self._reflected_count = 0 self._episode_index = 0 + self._mutations.clear() + self._distill_step = 0 # ------------------------------------------------------------------ # Internals @@ -425,6 +436,69 @@ def _make_question(self, inputs: dict) -> str: return self.question return format_inputs(inputs, self.budget.max_question_tokens) + def _record_mutations( + self, ops: list[Operation], new_ids: list[str] + ) -> list[Mutation]: + """Build Mutation records from operations and the new IDs generated by apply(). + + For DELETE/REPLACE, looks up pre-mutation state (section and previous_content). + For ADD, back-fills item_ids from new_ids in order. + + Args: + ops: Cartographer operations (ADD/DELETE/REPLACE). + new_ids: IDs of items created by apply() in the same order as ADD ops. + + Returns: + List of Mutation records for this step. + """ + mutations: list[Mutation] = [] + add_indices: list[int] = [] + for op in ops: + if op.type == OpType.DELETE and op.item_id: + found = self.cmap.find_item(op.item_id) + if found: + section, old_item = found + mutations.append( + Mutation( + step=self._distill_step, + type=OpType.DELETE, + item_id=op.item_id, + section=section, + content=None, + previous_content=old_item.content, + ) + ) + elif op.type == OpType.REPLACE and op.item_id and op.content: + found = self.cmap.find_item(op.item_id) + if found: + section, old_item = found + mutations.append( + Mutation( + step=self._distill_step, + type=OpType.REPLACE, + item_id=op.item_id, + section=section, + content=op.content, + previous_content=old_item.content, + ) + ) + elif op.type == OpType.ADD and op.section and op.content: + add_indices.append(len(mutations)) + mutations.append( + Mutation( + step=self._distill_step, + type=OpType.ADD, + item_id="", + section=op.section, + content=op.content, + previous_content=None, + ) + ) + # Back-fill ADD mutation item_ids from new_ids + for i, new_id in zip(add_indices, new_ids): + mutations[i].item_id = new_id + return mutations + def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, @@ -459,9 +533,12 @@ def _distill(self, trajectory: str, question: str) -> None: if ops: self.cmap, new_ids = self.cmap.apply(ops) + mutations = self._record_mutations(ops, new_ids) + self._mutations.extend(mutations) for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 + self._distill_step += 1 self.cmap = evict(self.cmap, self.scores, self.budget.max_context_map_tokens) live = self.cmap.ids() diff --git a/src/codespy/agents/memory/hippocampus/modules/__init__.py b/src/codespy/agents/memory/hippocampus/modules/__init__.py index ac608f7..4bb65b6 100644 --- a/src/codespy/agents/memory/hippocampus/modules/__init__.py +++ b/src/codespy/agents/memory/hippocampus/modules/__init__.py @@ -1,5 +1,6 @@ """DSPy modules for the hippocampus agent.""" +from codespy.agents.memory.hippocampus.context_map import Mutation from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig @@ -8,4 +9,5 @@ "CartographerSig", "Distiller", "DistillerSig", + "Mutation", ] From 26b83fa3ebcd3917fad00e399d9f269690bb7882 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 7 Aug 2026 22:16:30 +0200 Subject: [PATCH 31/79] wip --- .../agents/memory/hippocampus/context_map.py | 32 ++++++++++- .../agents/memory/hippocampus/hippocampus.py | 6 ++- src/codespy/agents/reviewer/models.py | 14 +++++ .../agents/reviewer/modules/auditor.py | 21 ++++---- .../agents/reviewer/modules/code_reviewer.py | 43 +++++++++------ .../agents/reviewer/modules/doc_reviewer.py | 43 +++++++++------ .../reviewer/modules/scope_identifier.py | 34 +++++++----- .../agents/reviewer/modules/summarizer.py | 14 +++-- .../reviewer/modules/supply_chain_auditor.py | 43 +++++++++------ src/codespy/agents/reviewer/reviewer.py | 53 ++++++++++++------- 10 files changed, 209 insertions(+), 94 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py index c0f0ce5..7c3740a 100644 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ b/src/codespy/agents/memory/hippocampus/context_map.py @@ -188,5 +188,35 @@ def to_json(self) -> str: @classmethod def from_json(cls, text: str) -> ContextMap: - """Deserialize a map from a JSON string produced by ``to_json()``.""" + """Deserialize a map from a JSON string produced by ``to_json()`` .""" return cls.model_validate_json(text) + + @classmethod + def merge(cls, *maps: "ContextMap") -> "ContextMap": + """Merge multiple context maps into a single map. + + Later maps win on ID collision (items with duplicate IDs are + replaced by those from later maps in the argument list). + + Args: + *maps: One or more ContextMap instances to merge. + + Returns: + A new ContextMap containing merged items from all input maps. + """ + merged = cls() + for cmap in maps: + for sec in cls.section_names(): + merged_section = merged.section(sec) + existing_ids = {item.id for item in merged_section} + for item in cmap.section(sec): + if item.id in existing_ids: + # Replace existing item (later wins) + merged_section[:] = [ + it if it.id != item.id else item.model_copy(deep=True) + for it in merged_section + ] + else: + merged_section.append(item.model_copy(deep=True)) + existing_ids.add(item.id) + return merged diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 0418dfd..19f4d5d 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -110,6 +110,7 @@ def __init__( question: str | None = None, task_name: str | None = None, run_id: str | None = None, + initial_memory: ContextMap | None = None, ): """ Args: @@ -143,6 +144,9 @@ class for per-field guidance. Resolve one from configuration with used as the ```` prefix in the episode filename (``-.json``) and recorded on ``Episode.run_id``. If ``None`` (standalone usage), a random UUID is generated. + initial_memory: Optional context map to seed the agent with. When + provided, the agent starts with this map instead of an empty one, + inheriting accumulated understanding from upstream pipeline stages. """ super().__init__() @@ -167,7 +171,7 @@ class for per-field guidance. Resolve one from configuration with self.budget = budget or MemoryBudget() self.max_reflects = max_reflects self.question = question - self.cmap = ContextMap() + self.cmap = initial_memory.model_copy(deep=True) if initial_memory else ContextMap() self.scores: dict[str, int] = {} # Buffer of per-call bounded trajectory strings, cleared after end_episode(). self._episode_trajectories: list[str] = [] diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index db486ed..2c8fba8 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -6,6 +6,8 @@ from pydantic import BaseModel, Field, field_validator +from codespy.agents.memory.hippocampus import ContextMap + class PRContext(BaseModel): """Shared PR identity passed to all review modules after summarization. @@ -21,6 +23,18 @@ class PRContext(BaseModel): summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") +class ReviewContext(BaseModel): + """Evolving pipeline state threaded through review stages. + + Carries both the immutable PR identity and the inherited context map + (memory) from upstream pipeline stages. Updated at each stage boundary + so downstream modules inherit accumulated understanding. + """ + + pr_context: PRContext = Field(description="Immutable PR identity (repo, number, title, summary)") + memory: ContextMap | None = Field(default=None, description="Inherited context map from upstream stages") + + class IssueSeverity(str, Enum): """Severity level of an issue.""" diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 3a10ceb..4e35f4e 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -7,7 +7,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, PRContext +from codespy.agents.reviewer.models import Issue, ReviewContext from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile @@ -53,7 +53,7 @@ def __init__(self) -> None: def forward( self, - pr_context: PRContext, + review_context: ReviewContext, changed_files: Sequence[ChangedFile], all_issues: Sequence[Issue], run_id: str | None = None, @@ -61,7 +61,7 @@ def forward( """Assess quality and recommend action. Args: - pr_context: PR context containing repo_slug, mr_number, mr_title, summary + review_context: ReviewContext containing PR identity and inherited memory changed_files: In-scope reviewable files all_issues: All issues found during review run_id: Pipeline run identifier @@ -80,8 +80,8 @@ def forward( logger.info("Running audit...") question = ( - f"final audit of {pr_context.repo_slug}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" + f"final audit of {review_context.pr_context.repo_slug}: " + f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" ) with SignatureContext("audit", self._cost_tracker): @@ -93,16 +93,17 @@ def forward( question=question, task_name="audit", run_id=run_id, + initial_memory=review_context.memory, ) result = mem( - mr_title=pr_context.mr_title, - summary=pr_context.summary, + mr_title=review_context.pr_context.mr_title, + summary=review_context.pr_context.summary, changed_files=list(changed_files), all_issues=list(all_issues), ) mem.end_episode( get_memory_store(self._settings), - f"/{pr_context.repo_slug}/", + f"/{review_context.pr_context.repo_slug}/", artifacts={ "audit": ( f"## Quality Assessment\n\n{result.quality_assessment}\n\n" @@ -112,8 +113,8 @@ def forward( ) else: result = auditor( - mr_title=pr_context.mr_title, - summary=pr_context.summary, + mr_title=review_context.pr_context.mr_title, + summary=review_context.pr_context.summary, changed_files=list(changed_files), all_issues=list(all_issues), ) diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 0f865a4..ae3197d 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -9,7 +9,8 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult +from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -184,8 +185,8 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for defects, security issues, and code smells. Args: @@ -193,14 +194,14 @@ async def aforward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of bug, security, and smell issues found across all scopes + Tuple of (list of issues, merged context map or None) """ if not self._settings.is_signature_enabled("code_review"): logger.debug("Skipping code_review: disabled") - return [] + return [], review_context.memory if review_context else None # Determine which categories are active categories: list[IssueCategory] = [] @@ -211,9 +212,10 @@ async def aforward( changed_scopes = [s for s in scopes if s.has_changes and s.changed_files] if not changed_scopes: logger.info("No scopes with changes for code review") - return [] + return [], review_context.memory if review_context else None all_issues: list[Issue] = [] + scope_memories: list[ContextMap] = [] max_iters = self._settings.get_max_iters("code_review") total_files = sum(len(s.changed_files) for s in changed_scopes) @@ -236,12 +238,13 @@ async def aforward( f" Code review: scope {scope.subroot} " f"({len(scope.changed_files)} files)" ) + mem: Hippocampus | None = None async with SignatureContext("code_review", self._cost_tracker): if self._settings.get_memory_enabled("code_review"): question = ( f"review code change of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None + f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) if review_context else None mem = Hippocampus( agent, budget=self._settings.get_memory_budget("code_review"), @@ -249,6 +252,7 @@ async def aforward( question=question, task_name="code_review", run_id=run_id, + initial_memory=review_context.memory if review_context else None, ) result = await mem.aforward( scope=scoped, @@ -263,6 +267,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) + # Collect scope's context map + if mem: + scope_memories.append(mem.cmap.model_copy(deep=True)) else: result = await agent.acall( scope=scoped, @@ -283,15 +290,21 @@ async def aforward( await cleanup_mcp_contexts(contexts) logger.info(f"Code review found {len(all_issues)} issues") - return all_issues + # Merge all scope context maps into one module-level map + merged_memory = ( + ContextMap.merge(*scope_memories) + if scope_memories + else (review_context.memory if review_context else None) + ) + return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for code issues (sync wrapper). Args: @@ -299,9 +312,9 @@ def forward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of bug, security, and smell issues found across all scopes + Tuple of (list of issues, merged context map or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 988a9ef..59bbfd3 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -9,7 +9,8 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult +from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -117,8 +118,8 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for documentation issues. Args: @@ -126,19 +127,20 @@ async def aforward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of documentation issues found across all scopes + Tuple of (list of issues, merged context map or None) """ if not self._settings.is_signature_enabled("doc"): logger.debug("Skipping doc: disabled") - return [] + return [], review_context.memory if review_context else None changed_scopes = [s for s in scopes if s.has_changes and s.changed_files] if not changed_scopes: logger.info("No scopes with changes for doc review") - return [] + return [], review_context.memory if review_context else None all_issues: list[Issue] = [] + scope_memories: list[ContextMap] = [] total_files = sum(len(s.changed_files) for s in changed_scopes) logger.info( f"Doc review for {len(changed_scopes)} scopes " @@ -170,12 +172,13 @@ async def aforward( f" Doc review: scope {scope.subroot} " f"({len(scope.changed_files)} files)" ) + mem: Hippocampus | None = None async with SignatureContext("doc", self._cost_tracker): if self._settings.get_memory_enabled("doc"): question = ( f"review documentation of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None + f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) if review_context else None mem = Hippocampus( reviewer, budget=self._settings.get_memory_budget("doc"), @@ -183,6 +186,7 @@ async def aforward( question=question, task_name="doc", run_id=run_id, + initial_memory=review_context.memory if review_context else None, ) result = await mem.aforward( patches=patches, @@ -198,6 +202,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) + # Collect scope's context map + if mem: + scope_memories.append(mem.cmap.model_copy(deep=True)) else: result = await asyncio.to_thread( reviewer, @@ -218,15 +225,21 @@ async def aforward( logger.error(f"Doc review failed for scope {scope.subroot}: {e}") logger.info(f"Doc review found {len(all_issues)} issues") - return all_issues + # Merge all scope context maps into one module-level map + merged_memory = ( + ContextMap.merge(*scope_memories) + if scope_memories + else (review_context.memory if review_context else None) + ) + return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for documentation issues (sync wrapper). Args: @@ -234,9 +247,9 @@ def forward( repo_path: Path to the cloned repository run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of documentation issues found across all scopes + Tuple of (list of issues, merged context map or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 7451194..ed900e0 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -10,7 +10,8 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import PackageManifest, PRContext, ScopeResult, ScopeType +from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.reviewer.models import PackageManifest, ReviewContext, ScopeResult, ScopeType from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file @@ -237,8 +238,8 @@ async def aforward( repo_path: Path, is_local: bool = False, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[ScopeResult]: + review_context: ReviewContext | None = None, + ) -> tuple[list[ScopeResult], ContextMap | None]: """Identify scopes in the repository for the given MR. Args: @@ -247,7 +248,10 @@ async def aforward( is_local: If True, repo is already on disk (skip cloning) run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) - pr_context: PR context used to construct Hippocampus question + review_context: ReviewContext containing PR identity and inherited memory + + Returns: + Tuple of (list of ScopeResult, final context map or None) """ # Get excluded directories from settings excluded_dirs = self._settings.excluded_directories @@ -283,7 +287,7 @@ async def aforward( package_manifest=None, changed_files=reviewable_files, reason="Scope identification disabled - fallback to single scope", - )] + )], review_context.memory if review_context else None tools, contexts = await self._create_mcp_tools(repo_path, is_local=is_local) changed_file_paths = [f.filename for f in reviewable_files] @@ -302,13 +306,14 @@ async def aforward( max_iters=max_iters, ) logger.info(f"Identifying scopes for {len(changed_file_paths)} changed files...") - # Track scope signature costs + mem: Hippocampus | None = None + final_memory: ContextMap | None = review_context.memory if review_context else None async with SignatureContext("scope", self._cost_tracker): if self._settings.get_memory_enabled("scope"): question = ( - f"identify scopes of {pr_context.repo_slug}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None + f"identify scopes of {review_context.pr_context.repo_slug}: " + f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) if review_context else None mem = Hippocampus( agent, budget=self._settings.get_memory_budget("scope"), @@ -316,6 +321,7 @@ async def aforward( question=question, task_name="scope", run_id=run_id, + initial_memory=review_context.memory if review_context else None, ) result = await mem.aforward( changed_files=changed_file_paths, @@ -355,6 +361,8 @@ async def aforward( scopes = self._convert_assignments_to_results( scope_assignments, changed_files_map, repo ) + # Capture final memory after successful execution + final_memory = mem.cmap.model_copy(deep=True) if mem else (review_context.memory if review_context else None) except Exception as e: logger.error(f"Agent failed: {e}") scopes = [ScopeResult( @@ -374,7 +382,7 @@ async def aforward( # Log results total_files = sum(len(s.changed_files) for s in scopes) logger.info(f"Identified {len(scopes)} scopes covering {total_files} files") - return scopes + return scopes, final_memory def _convert_assignments_to_results( self, @@ -421,7 +429,7 @@ def forward( repo_path: Path, is_local: bool = False, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[ScopeResult]: + review_context: ReviewContext | None = None, + ) -> tuple[list[ScopeResult], ContextMap | None]: """Identify scopes (sync wrapper).""" - return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id, pr_context=pr_context)) + return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_context)) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 6ded4ed..db8aa45 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -5,7 +5,7 @@ import dspy from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus +from codespy.agents.memory.hippocampus import ContextMap, Hippocampus from codespy.config import get_settings from codespy.config_memory import get_memory_store @@ -47,7 +47,7 @@ def forward( changed_file_paths: list[str], repo_slug: str, run_id: str | None = None, - ) -> str: + ) -> tuple[str, ContextMap | None]: """Generate a PR summary. Args: @@ -59,17 +59,19 @@ def forward( run_id: Pipeline run identifier Returns: - The summary string (2-3 sentences) + Tuple of (summary string, final context map or None) """ + if not self._settings.is_signature_enabled("summary"): logger.debug("Skipping summary: disabled") - return mr_title or "No title" + return mr_title or "No title", None summarizer = dspy.ChainOfThought(PRSummarySignature) logger.info("Generating PR summary...") question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" + mem: Hippocampus | None = None with SignatureContext("summary", self._cost_tracker): if self._settings.get_memory_enabled("summary"): mem = Hippocampus( @@ -98,4 +100,6 @@ def forward( ) logger.info(f"PR summary: {result.summary[:80]}...") - return result.summary + # Return final context map when memory is enabled + final_memory = mem.cmap.model_copy(deep=True) if mem else None + return result.summary, final_memory diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 9a264f6..37a0cf6 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -9,7 +9,8 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.reviewer.models import Issue, IssueCategory, PRContext, ScopeResult +from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -240,8 +241,8 @@ async def aforward( scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for supply chain security vulnerabilities and return issues. For each scope, filesystem/parser tools are created rooted at @@ -254,22 +255,23 @@ async def aforward( repo_path: Path to the cloned repository for reading manifest files run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of security issues found across all scopes + Tuple of (list of issues, merged context map or None) """ # Check if supply chain signature is enabled if not self._settings.is_signature_enabled("supply_chain"): logger.debug("Skipping supply_chain: disabled") - return [] + return [], review_context.memory if review_context else None # Check if any scope has supply-chain-relevant changes if not self._needs_analysis(scopes): logger.info("Skipping supply chain analysis: no dependency changes or Dockerfiles modified") - return [] + return [], review_context.memory if review_context else None all_issues: list[Issue] = [] + scope_memories: list[ContextMap] = [] supply_chain_max_iters = self._settings.get_max_iters("supply_chain") # Create OSV tools once (shared across scopes, no filesystem root) @@ -322,12 +324,13 @@ async def aforward( f"manifest={bool(manifest_path)}" ) # Track supply_chain signature costs separately + mem: Hippocampus | None = None async with SignatureContext("supply_chain", self._cost_tracker): if self._settings.get_memory_enabled("supply_chain"): question = ( f"review supply chain of {scope.repo}: {scope.subroot}: " - f"pull request {pr_context.mr_number} {pr_context.mr_title}: {pr_context.summary}" - ) if pr_context else None + f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) if review_context else None mem = Hippocampus( supply_chain_agent, budget=self._settings.get_memory_budget("supply_chain"), @@ -337,6 +340,7 @@ async def aforward( question=question, task_name="supply_chain", run_id=run_id, + initial_memory=review_context.memory if review_context else None, ) result = await mem.aforward( manifest_path=manifest_path, @@ -353,6 +357,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) + # Collect scope's context map + if mem: + scope_memories.append(mem.cmap.model_copy(deep=True)) else: result = await supply_chain_agent.acall( manifest_path=manifest_path, @@ -376,15 +383,21 @@ async def aforward( await cleanup_mcp_contexts(osv_contexts) logger.info(f"Security audit found {len(all_issues)} issues") - return all_issues + # Merge all scope context maps into one module-level map + merged_memory = ( + ContextMap.merge(*scope_memories) + if scope_memories + else (review_context.memory if review_context else None) + ) + return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], repo_path: Path, run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], ContextMap | None]: """Analyze scopes for supply chain security vulnerabilities (sync wrapper). Args: @@ -392,9 +405,9 @@ def forward( repo_path: Path to the cloned repository for reading manifest files run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run - pr_context: PR context used to construct Hippocampus question per scope + review_context: ReviewContext containing PR identity and inherited memory Returns: - List of security issues found across all scopes + Tuple of (list of issues, merged context map or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, pr_context=pr_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 3606826..33c4938 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -12,9 +12,11 @@ from codespy.config import Settings, get_settings from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff +from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import ( Issue, PRContext, + ReviewContext, SignatureStatsResult, ReviewResult, ReviewConfig, @@ -88,8 +90,8 @@ async def _run_review_modules( repo_path: Path, module_names: list[str], run_id: str | None = None, - pr_context: PRContext | None = None, - ) -> list[Issue]: + review_context: ReviewContext | None = None, + ) -> tuple[list[Issue], dict[str, ContextMap | None]]: """Run review modules concurrently in a single event loop. Uses asyncio.gather instead of dspy.Parallel to avoid the @@ -102,25 +104,28 @@ async def _run_review_modules( module_names: Names of modules (for error logging) run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run - pr_context: PR context for Hippocampus question (passed to all modules) + review_context: ReviewContext for Hippocampus question and memory inheritance Returns: - Aggregated list of issues from all modules + Tuple of (aggregated list of issues, dict of module_name -> context_map) """ tasks = [ - self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), - self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), - self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, pr_context=pr_context), + self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), + self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), + self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), ] results = await asyncio.gather(*tasks, return_exceptions=True) all_issues: list[Issue] = [] + context_maps: dict[str, ContextMap | None] = {} for i, result in enumerate(results): if isinstance(result, Exception): logger.error(f"{module_names[i]} failed: {result}") elif result is not None: - all_issues.extend(result) - return all_issues + issues, ctx_map = result + all_issues.extend(issues) + context_maps[module_names[i]] = ctx_map + return all_issues, context_maps def _build_local_mr(self, config: LocalReviewConfig) -> MergeRequest: """Build a MergeRequest from local git changes. @@ -173,7 +178,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Step 1: Run Summarizer (before scope identification) changed_file_paths = [f.filename for f in mr.changed_files] - pr_summary = self.summarizer( + pr_summary, summarizer_memory = self.summarizer( mr_title=mr.title, mr_description=mr.body or "No description provided.", mr_number=mr.number, @@ -182,19 +187,21 @@ def forward(self, config: ReviewConfig) -> ReviewResult: run_id=run_id, ) - # Build PRContext after summarizer runs + # Build PRContext and ReviewContext after summarizer runs pr_context = PRContext( repo_slug=mr.repo_slug, mr_number=mr.number, mr_title=mr.title, summary=pr_summary, ) + # Summarizer is first stage - no inherited memory yet + review_ctx = ReviewContext(pr_context=pr_context, memory=summarizer_memory) - # Step 2: Identify scopes (pass pr_context) + # Step 2: Identify scopes (inherits Summarizer memory) is_local = isinstance(config, LocalReviewConfig) logger.info("Identifying code scopes...") - scopes = self.scope_identifier( - mr, repo_path, is_local=is_local, run_id=run_id, pr_context=pr_context + scopes, scope_memory = self.scope_identifier( + mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_ctx ) for scope in scopes: logger.info(f" Scope: {scope.subroot} ({scope.scope_type.value}) - {len(scope.changed_files)} files") @@ -206,22 +213,30 @@ def forward(self, config: ReviewConfig) -> ReviewResult: if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") - # Step 3: Run review modules concurrently via asyncio.gather (pass pr_context) + # Update ReviewContext with Scope Identifier's memory for downstream modules + review_ctx = ReviewContext(pr_context=pr_context, memory=scope_memory) + + # Step 3: Run review modules concurrently via asyncio.gather (inherit Scope Identifier memory) module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") - all_issues = asyncio.run( - self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, pr_context=pr_context) + all_issues, parallel_memories = asyncio.run( + self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, review_context=review_ctx) ) logger.info(f"Found {len(all_issues)} issues") - # Step 4: Run Audit + # Merge parallel context maps for Auditor + maps_to_merge = [m for m in parallel_memories.values() if m is not None] + merged_memory = ContextMap.merge(*maps_to_merge) if maps_to_merge else scope_memory + review_ctx = ReviewContext(pr_context=pr_context, memory=merged_memory) + + # Step 4: Run Audit (inherits merged memory from parallel modules) scoped_files = self._collect_scoped_files(scopes) logger.info( f"Audit input: {len(scoped_files)} in-scope files " f"(filtered from {len(mr.changed_files)} total)" ) quality_assessment, recommendation = self.auditor( - pr_context=pr_context, + review_context=review_ctx, changed_files=scoped_files, all_issues=all_issues, run_id=run_id, From 99b95e4adc0624d6f115945505dde47392d67462 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 7 Aug 2026 23:13:52 +0200 Subject: [PATCH 32/79] treesitter upgrade --- .../parsers/treesitter/extractors/__init__.py | 10 + .../parsers/treesitter/extractors/bash.py | 94 ++++ .../parsers/treesitter/extractors/cpp.py | 124 ++++++ .../parsers/treesitter/extractors/csharp.py | 140 ++++++ .../parsers/treesitter/extractors/php.py | 171 ++++++++ .../treesitter/extractors/regex_fallback.py | 411 ++++++++++++++++++ .../parsers/treesitter/extractors/ruby.py | 132 ++++++ .../tools/parsers/treesitter/parser.py | 59 ++- 8 files changed, 1140 insertions(+), 1 deletion(-) create mode 100644 src/codespy/tools/parsers/treesitter/extractors/bash.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/cpp.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/csharp.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/php.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/ruby.py diff --git a/src/codespy/tools/parsers/treesitter/extractors/__init__.py b/src/codespy/tools/parsers/treesitter/extractors/__init__.py index 88d270a..40d5df1 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/__init__.py +++ b/src/codespy/tools/parsers/treesitter/extractors/__init__.py @@ -1,22 +1,32 @@ """Language-specific extractors for tree-sitter parsing.""" +from codespy.tools.parsers.treesitter.extractors.bash import BashExtractor +from codespy.tools.parsers.treesitter.extractors.cpp import CppExtractor +from codespy.tools.parsers.treesitter.extractors.csharp import CSharpExtractor from codespy.tools.parsers.treesitter.extractors.go import GoExtractor from codespy.tools.parsers.treesitter.extractors.java import JavaExtractor from codespy.tools.parsers.treesitter.extractors.javascript import JavaScriptExtractor from codespy.tools.parsers.treesitter.extractors.kotlin import KotlinExtractor from codespy.tools.parsers.treesitter.extractors.objc import ObjCExtractor +from codespy.tools.parsers.treesitter.extractors.php import PHPExtractor from codespy.tools.parsers.treesitter.extractors.python import PythonExtractor +from codespy.tools.parsers.treesitter.extractors.ruby import RubyExtractor from codespy.tools.parsers.treesitter.extractors.rust import RustExtractor from codespy.tools.parsers.treesitter.extractors.swift import SwiftExtractor from codespy.tools.parsers.treesitter.extractors.terraform import TerraformExtractor __all__ = [ + "BashExtractor", + "CppExtractor", + "CSharpExtractor", "GoExtractor", "JavaExtractor", "JavaScriptExtractor", "KotlinExtractor", "ObjCExtractor", + "PHPExtractor", "PythonExtractor", + "RubyExtractor", "RustExtractor", "SwiftExtractor", "TerraformExtractor", diff --git a/src/codespy/tools/parsers/treesitter/extractors/bash.py b/src/codespy/tools/parsers/treesitter/extractors/bash.py new file mode 100644 index 0000000..8d02085 --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/bash.py @@ -0,0 +1,94 @@ +"""Bash/Shell function extractor.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +class BashExtractor(BaseExtractor): + """Extract function definitions from Bash/Shell source code.""" + + def extract_functions( + self, + node: Any, + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract Bash function definitions.""" + functions: list[FunctionInfo] = [] + + def visit(n: Any) -> None: + if n.type == "function_definition": + func_info = self._extract_function_info(n, file_path, source) + if func_info: + functions.append(func_info) + + for child in n.children: + visit(child) + + visit(node) + return functions + + def _extract_function_info( + self, + func_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract function info from a function_definition node.""" + name_node = func_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + + # Bash functions don't have typed parameters or return types + # Parameters are accessed via $1, $2, etc. + # We can try to extract parameter count from the body + params = self._extract_bash_params(func_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=func_node.start_point[0] + 1, + line_end=func_node.end_point[0] + 1, + parameters=params, + return_type=None, # Bash doesn't have explicit return types + is_method=False, + ) + + def _extract_bash_params(self, node: Any, source: bytes) -> list[str]: + """Extract Bash function parameters from the body. + + Bash uses positional parameters ($1, $2, etc.) so we look for + references to determine how many parameters the function uses. + """ + params: list[str] = [] + seen_params: set[int] = set() + + def find_param_refs(n: Any) -> None: + if n.type == "special_variable_name": + text = self._get_node_text(n, source) + if text.startswith("$"): + try: + param_num = int(text[1:]) + if param_num > 0: + seen_params.add(param_num) + except ValueError: + pass + for child in n.children: + find_param_refs(child) + + find_param_refs(node) + + # Build parameter list based on what we found + if seen_params: + max_param = max(seen_params) + for i in range(1, max_param + 1): + params.append(f"${i}") + + return params diff --git a/src/codespy/tools/parsers/treesitter/extractors/cpp.py b/src/codespy/tools/parsers/treesitter/extractors/cpp.py new file mode 100644 index 0000000..035ad5e --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/cpp.py @@ -0,0 +1,124 @@ +"""C/C++ function extractor.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +class CppExtractor(BaseExtractor): + """Extract function definitions from C/C++ source code.""" + + def extract_functions( + self, + node: Any, + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract C/C++ function definitions.""" + functions: list[FunctionInfo] = [] + + def visit(n: Any) -> None: + if n.type == "function_definition": + decl_node = n.child_by_field_name("declarator") + if decl_node: + func_info = self._extract_function_info(decl_node, n, file_path, source) + if func_info: + functions.append(func_info) + + for child in n.children: + visit(child) + + visit(node) + return functions + + def _extract_function_info( + self, + declarator: Any, + function_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract function info from a function declarator node.""" + # Get function name from declarator + # The declarator can be: function_declarator, identifier, etc. + name_node = None + params = [] + return_type = None + + if declarator.type == "function_declarator": + # Get name from the declarator + name_part = declarator.child_by_field_name("declarator") + if name_part: + if name_part.type == "identifier": + name_node = name_part + elif name_part.type == "field_identifier": + name_node = name_part + elif name_part.type == "qualified_identifier": + # C++ class method: Class::method + # Get the last part + name_node = name_part + + # Extract parameters + params_node = declarator.child_by_field_name("parameters") + if params_node: + params = self._extract_cpp_params(params_node, source) + + elif declarator.type == "identifier": + name_node = declarator + # Look for parameters in parent function_definition + params_node = function_node.child_by_field_name("declarator") + if params_node and params_node.type == "function_declarator": + params_list = params_node.child_by_field_name("parameters") + if params_list: + params = self._extract_cpp_params(params_list, source) + + elif declarator.type == "field_identifier": + name_node = declarator + + if not name_node: + return None + + name = self._get_node_text(name_node, source) + + # Try to extract return type from the function_definition + type_node = function_node.child_by_field_name("type") + if type_node: + return_type = self._get_node_text(type_node, source).strip() + else: + # For constructors/destructors, return type is None + pass + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=function_node.start_point[0] + 1, + line_end=function_node.end_point[0] + 1, + parameters=params, + return_type=return_type if return_type else None, + is_method="::" in name or self._is_in_class_context(function_node), + ) + + def _extract_cpp_params(self, params_node: Any, source: bytes) -> list[str]: + """Extract C/C++ function parameters.""" + params: list[str] = [] + for child in params_node.children: + if child.type in ("parameter_declaration", "parameter_list"): + param_text = source[child.start_byte:child.end_byte].decode().strip() + # Clean up the parameter text + param_text = param_text.strip("()") + if param_text and param_text != "void": + params.append(param_text) + return params + + def _is_in_class_context(self, node: Any) -> bool: + """Check if function is inside a class/struct context.""" + current = node + while current: + if current.type in ("class_specifier", "struct_specifier", "namespace_definition"): + return True + current = current.parent + return False diff --git a/src/codespy/tools/parsers/treesitter/extractors/csharp.py b/src/codespy/tools/parsers/treesitter/extractors/csharp.py new file mode 100644 index 0000000..08fce40 --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/csharp.py @@ -0,0 +1,140 @@ +"""C# function/method extractor.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +class CSharpExtractor(BaseExtractor): + """Extract method definitions from C# source code.""" + + def extract_functions( + self, + node: Any, + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract C# method definitions.""" + functions: list[FunctionInfo] = [] + + def visit(n: Any, in_type: bool = False) -> None: + if n.type == "method_declaration": + func_info = self._extract_method_info(n, file_path, source) + if func_info: + functions.append(func_info) + + elif n.type == "constructor_declaration": + func_info = self._extract_constructor_info(n, file_path, source) + if func_info: + functions.append(func_info) + + elif n.type == "local_function_statement": + func_info = self._extract_local_function_info(n, file_path, source) + if func_info: + functions.append(func_info) + + for child in n.children: + # Track if we're inside a class/interface/struct + is_type = n.type in ("class_declaration", "interface_declaration", "struct_declaration", "record_declaration") + visit(child, in_type or is_type) + + visit(node) + return functions + + def _extract_method_info( + self, + method_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract method info from a method_declaration node.""" + name_node = method_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_csharp_params(method_node, source) + return_type = self._extract_csharp_return_type(method_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=method_node.start_point[0] + 1, + line_end=method_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=True, + ) + + def _extract_constructor_info( + self, + ctor_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract constructor info.""" + name_node = ctor_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_csharp_params(ctor_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=ctor_node.start_point[0] + 1, + line_end=ctor_node.end_point[0] + 1, + parameters=params, + return_type=None, # Constructors return the class type + is_method=True, + ) + + def _extract_local_function_info( + self, + func_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract local function info (C# 7.0+).""" + name_node = func_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_csharp_params(func_node, source) + return_type = self._extract_csharp_return_type(func_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=func_node.start_point[0] + 1, + line_end=func_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=False, # Local functions are not methods + ) + + def _extract_csharp_params(self, node: Any, source: bytes) -> list[str]: + """Extract C# method parameters.""" + params: list[str] = [] + params_node = node.child_by_field_name("parameters") + if params_node: + for child in params_node.children: + if child.type == "parameter": + # Extract parameter info + param_text = self._get_node_text(child, source).strip() + if param_text and param_text not in ("(", ")", ","): + params.append(param_text) + return params + + def _extract_csharp_return_type(self, node: Any, source: bytes) -> str | None: + """Extract C# method return type.""" + type_node = node.child_by_field_name("type") + if type_node: + return self._get_node_text(type_node, source).strip() + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/php.py b/src/codespy/tools/parsers/treesitter/extractors/php.py new file mode 100644 index 0000000..a154549 --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/php.py @@ -0,0 +1,171 @@ +"""PHP function extractor.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +class PHPExtractor(BaseExtractor): + """Extract function definitions from PHP source code.""" + + def extract_functions( + self, + node: Any, + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract PHP function definitions.""" + functions: list[FunctionInfo] = [] + + def visit(n: Any, in_class: bool = False) -> None: + if n.type == "function_definition": + func_info = self._extract_function_info(n, file_path, source, in_class) + if func_info: + functions.append(func_info) + + elif n.type == "method_declaration": + func_info = self._extract_method_info(n, file_path, source) + if func_info: + functions.append(func_info) + + elif n.type == "anonymous_function": + func_info = self._extract_anonymous_function_info(n, file_path, source) + if func_info: + functions.append(func_info) + + elif n.type == "arrow_function": + func_info = self._extract_arrow_function_info(n, file_path, source) + if func_info: + functions.append(func_info) + + for child in n.children: + # Track if we're inside a class + is_class = n.type in ("class_declaration", "interface_declaration", "trait_declaration") + visit(child, in_class or is_class) + + visit(node) + return functions + + def _extract_function_info( + self, + func_node: Any, + file_path: Path, + source: bytes, + in_class: bool, + ) -> FunctionInfo | None: + """Extract function info from a function_definition node.""" + name_node = func_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_php_params(func_node, source) + return_type = self._extract_php_return_type(func_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=func_node.start_point[0] + 1, + line_end=func_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=False, # Functions are not methods + ) + + def _extract_method_info( + self, + method_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract method info from a method_declaration node.""" + name_node = method_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_php_params(method_node, source) + return_type = self._extract_php_return_type(method_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=method_node.start_point[0] + 1, + line_end=method_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=True, + ) + + def _extract_anonymous_function_info( + self, + func_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract anonymous function (closure) info.""" + # Anonymous functions don't have a name, use placeholder + params = self._extract_php_params(func_node, source) + return_type = self._extract_php_return_type(func_node, source) + + return FunctionInfo( + name="(anonymous)", + file=str(file_path), + line_start=func_node.start_point[0] + 1, + line_end=func_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=False, + ) + + def _extract_arrow_function_info( + self, + func_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract arrow function (fn() => expr) info.""" + params = self._extract_php_params(func_node, source) + return_type = self._extract_php_return_type(func_node, source) + + return FunctionInfo( + name="(arrow)", + file=str(file_path), + line_start=func_node.start_point[0] + 1, + line_end=func_node.end_point[0] + 1, + parameters=params, + return_type=return_type, + is_method=False, + ) + + def _extract_php_params(self, node: Any, source: bytes) -> list[str]: + """Extract PHP function parameters.""" + params: list[str] = [] + params_node = node.child_by_field_name("parameters") + if not params_node: + return params + + for child in params_node.children: + if child.type == "parameter": + # Extract parameter info + param_text = self._get_node_text(child, source).strip() + if param_text and param_text not in ("(", ")", ","): + # Extract just the variable name if possible + var_node = child.child_by_field_name("name") + if var_node: + params.append(self._get_node_text(var_node, source)) + else: + params.append(param_text) + + return params + + def _extract_php_return_type(self, node: Any, source: bytes) -> str | None: + """Extract PHP function return type.""" + type_node = node.child_by_field_name("return_type") + if type_node: + return self._get_node_text(type_node, source).strip() + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py b/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py new file mode 100644 index 0000000..b7714f7 --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py @@ -0,0 +1,411 @@ +"""Regex-based function extractor for languages without tree-sitter grammars. + +This module provides lightweight pattern matching as a fallback when tree-sitter +parsers are not available. It uses ripgrep for fast line-based searching combined +with heuristics to identify function definitions and extract signatures. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +@dataclass +class LanguagePattern: + """Pattern configuration for a language.""" + + name: str + extensions: set[str] + # Pattern to match function definition line + function_pattern: re.Pattern + # Pattern to extract parameters from the definition line + param_pattern: re.Pattern | None = None + # Pattern to detect end of function (e.g., closing brace) + end_pattern: re.Pattern | None = None + # Comment characters to strip from signatures + comment_prefix: str | None = None + + +class RegexFallbackExtractor(BaseExtractor): + """Fallback extractor using regex patterns for unsupported languages. + + Supported languages: + - C/C++ (.c, .cpp, .h, .hpp) + - C# (.cs) + - Ruby (.rb) + - PHP (.php) + - Shell/Bash (.sh, .bash) + - SQL (.sql) - basic stored procedure detection + """ + + # Language patterns for function detection + PATTERNS: dict[str, LanguagePattern] = { + "c_cpp": LanguagePattern( + name="C/C++", + extensions={".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hh", ".hxx"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:static\s+|inline\s+|extern\s+|virtual\s+|explicit\s+|constexpr\s+|consteval\s+)*' # modifiers + r'(?:[\w:<>,\s\*&]+?\s+)?' # return type with templates/pointers + r'(\w+)' # function name (capture group 1) + r'\s*\([^)]*\)' # parameters + r'(?:\s*const)?' # optional const + r'(?:\s*->\s*[\w:<>,\s\*&]+)?' # optional trailing return (C++) + r'\s*[{;]', # opening brace or semicolon + re.MULTILINE, + ), + param_pattern=re.compile(r'\(([^)]*)\)'), + end_pattern=re.compile(r'^[\s]*}'), + comment_prefix="//", + ), + "csharp": LanguagePattern( + name="C#", + extensions={".cs"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|virtual\s+|' + r'override\s+|abstract\s+|sealed\s+|async\s+|unsafe\s+|extern\s+)*' # modifiers + r'(?:[\w<>,\s\[\]]+?\s+)' # return type (including generic/Task types) + r'(\w+)' # function name (capture group 1) + r'\s*\([^)]*\)' # parameters + r'(?:\s*where\s+\w+\s*:\s*[\w<>,\s]+)?' # optional generic constraint + r'\s*[{(]', # opening brace or expression body + re.MULTILINE, + ), + param_pattern=re.compile(r'\(([^)]*)\)'), + end_pattern=re.compile(r'^[\s]*[}]'), + comment_prefix="//", + ), + "ruby": LanguagePattern( + name="Ruby", + extensions={".rb", ".rbw", ".rake", ".gemspec"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:private\s+|protected\s+|public\s+)?' # visibility modifiers + r'def\s+' # def keyword + r'(?:self\.)?' # optional self. + r'(\w+[?!=]?)' # function name with optional ? ! = (capture group 1) + r'(?:\s*\([^)]*\))?' # optional parentheses with params + r'(?:\s+|$)', # whitespace or end of line + re.MULTILINE, + ), + # Ruby params are complex (block syntax, etc), keep simple + param_pattern=re.compile(r'def\s+(?:self\.)?\w+\s*\(([^)]*)\)'), + end_pattern=re.compile(r'^[\s]*end\s*$'), + comment_prefix="#", + ), + "php": LanguagePattern( + name="PHP", + extensions={".php", ".php4", ".php5", ".phtml"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:public\s+|private\s+|protected\s+)?' # visibility + r'(?:static\s+)?' # optional static + r'(?:abstract\s+|final\s+)?' # optional abstract/final + r'function\s+' # function keyword + r'(&)?' # optional reference return + r'(\w+)' # function name (capture group 2, 1 is &) + r'\s*\([^)]*\)', # parameters + re.MULTILINE, + ), + param_pattern=re.compile(r'\(([^)]*)\)'), + end_pattern=re.compile(r'^[\s]*}'), + comment_prefix="//", # Also supports # but // is more common + ), + "shell": LanguagePattern( + name="Shell/Bash", + extensions={".sh", ".bash", ".zsh", ".ksh", ".dash"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:function\s+)?' # optional function keyword + r'(\w+)' # function name (capture group 1) + r'\s*\(\s*\)' # empty parentheses + r'\s*\{', # opening brace + re.MULTILINE, + ), + # Shell functions typically don't declare params in signature + param_pattern=None, + end_pattern=re.compile(r'^[\s]*}'), + comment_prefix="#", + ), + "sql": LanguagePattern( + name="SQL", + extensions={".sql"}, + function_pattern=re.compile( + r'^[\s]*' # leading whitespace + r'(?:CREATE\s+OR\s+REPLACE\s+)?' # optional create or replace + r'(?:CREATE\s+)?' # optional create + r'(?:PROCEDURE|FUNCTION|TRIGGER|EVENT)\s+' # object type + r'(?:[\w.]+\s+)?' # optional schema prefix + r'(\w+)' # name (capture group 1) + r'\s*\(', # opening paren for params + re.MULTILINE | re.IGNORECASE, + ), + param_pattern=re.compile(r'\(([^)]*)\)'), + end_pattern=re.compile(r'^[\s]*END\s*;?', re.IGNORECASE), + comment_prefix="--", + ), + } + + def __init__(self) -> None: + """Initialize the extractor.""" + # Build extension -> pattern mapping for fast lookup + self._ext_to_pattern: dict[str, LanguagePattern] = {} + for pattern in self.PATTERNS.values(): + for ext in pattern.extensions: + self._ext_to_pattern[ext.lower()] = pattern + + def _get_pattern(self, file_path: Path) -> LanguagePattern | None: + """Get pattern for file based on extension.""" + ext = file_path.suffix.lower() + return self._ext_to_pattern.get(ext) + + def _strip_comments(self, line: str, comment_prefix: str | None) -> str: + """Remove inline comments from a line.""" + if not comment_prefix: + return line + # Handle both // and # style comments + if comment_prefix in line: + return line.split(comment_prefix)[0].rstrip() + return line + + def _extract_params(self, line: str, pattern: LanguagePattern) -> list[str]: + """Extract parameter names from function signature.""" + if not pattern.param_pattern: + return [] + + match = pattern.param_pattern.search(line) + if not match: + return [] + + params_str = match.group(1).strip() + if not params_str: + return [] + + # Simple splitting - handles common cases + # For complex cases (templates, function pointers), just return raw params + params = [] + current_param = "" + depth = 0 + + for char in params_str: + if char in "(<{": + depth += 1 + current_param += char + elif char in ")>}: + depth -= 1 + current_param += char + elif char == "," and depth == 0: + # End of parameter + param = current_param.strip() + if param: + # Extract parameter name (last word before any =) + param_name = self._extract_param_name(param) + if param_name: + params.append(param_name) + current_param = "" + else: + current_param += char + + # Handle last parameter + if current_param.strip(): + param_name = self._extract_param_name(current_param.strip()) + if param_name: + params.append(param_name) + + return params + + def _extract_param_name(self, param: str) -> str | None: + """Extract parameter name from parameter declaration. + + Examples: + - "int x" -> "x" + - "const std::string& name" -> "name" + - "int x = 5" -> "x" + - "std::vector items" -> "items" + """ + # Remove default values + if "=" in param: + param = param.split("=")[0].strip() + + # Split by whitespace and take last part + # Handle pointers/references by stripping * and & + parts = param.split() + if not parts: + return None + + name_part = parts[-1] + # Strip *, &, etc from the end + name = name_part.rstrip("*&").strip() + + # Validate it's a reasonable identifier + if re.match(r'^[a-zA-Z_]\w*$', name): + return name + return None + + def _find_function_end( + self, + lines: list[str], + start_line: int, + pattern: LanguagePattern, + ) -> int: + """Find the approximate end line of a function. + + Uses brace counting for C-style languages or end keyword detection. + """ + if not pattern.end_pattern and not pattern.function_pattern: + return start_line + + brace_depth = 0 + in_function = False + + for i, line in enumerate(lines[start_line - 1:], start=start_line): + stripped = line.strip() + + if not in_function: + # Look for opening brace to enter function + if "{" in line: + brace_depth = line.count("{") - line.count("}") + in_function = True + elif pattern.name in ("ruby", "sql") and stripped.startswith("def "): + in_function = True + continue + + # In function body - track braces + brace_depth += line.count("{") - line.count("}") + + # Check for end pattern + if pattern.end_pattern and pattern.end_pattern.match(line): + return i + + # For brace-based languages, depth reaching 0 means end + if pattern.name not in ("ruby", "sql") and brace_depth <= 0: + return i + + # Safety limit - don't search forever + if i - start_line > 500: + return start_line + 100 + + return min(start_line + 50, len(lines)) + + def extract_functions( + self, + root_node: Any, # Not used, for compatibility with BaseExtractor + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract function definitions using regex patterns. + + Args: + root_node: Not used (for compatibility) + file_path: Path to the source file + source: File content as bytes + + Returns: + List of FunctionInfo objects + """ + pattern = self._get_pattern(file_path) + if not pattern: + return [] + + try: + content = source.decode("utf-8", errors="ignore") + except Exception: + return [] + + lines = content.split("\n") + functions = [] + seen_lines: set[int] = set() # Track to avoid duplicates + + for match in pattern.function_pattern.finditer(content): + name = match.group(1) + if not name: + continue + + # Calculate line number + line_start = content[: match.start()].count("\n") + 1 + + # Skip if we've seen this line (can happen with overlapping patterns) + if line_start in seen_lines: + continue + seen_lines.add(line_start) + + # Get the full line for parameter extraction + line_idx = line_start - 1 + if line_idx >= len(lines): + continue + + line = lines[line_idx] + clean_line = self._strip_comments(line, pattern.comment_prefix) + + # Extract parameters + params = self._extract_params(clean_line, pattern) + + # Find approximate end line + line_end = self._find_function_end(lines, line_start, pattern) + + # Determine return type (heuristic) + return_type = None + if pattern.name in ("c_cpp", "csharp"): + # Try to extract return type from before function name + func_match = pattern.function_pattern.match(clean_line) + if func_match: + prefix = clean_line[: func_match.start(1)].strip() + # Remove modifiers + for mod in ["static", "inline", "extern", "virtual", "explicit", + "constexpr", "consteval", "public", "private", + "protected", "internal", "async", "abstract", + "sealed", "unsafe", "override"]: + prefix = re.sub(rf"\b{mod}\b\s*", "", prefix) + return_type = prefix.strip() if prefix.strip() else None + + functions.append( + FunctionInfo( + name=name, + file=str(file_path), + line_start=line_start, + line_end=line_end, + parameters=params, + return_type=return_type, + is_method=False, # Could be refined + receiver_type=None, + docstring=None, + ) + ) + + return functions + + def extract_signatures( + self, + file_path: Path, + source: bytes, + ) -> dict[str, str]: + """Extract function signatures as strings. + + Returns a mapping of function name -> signature string + for use in hunks+metadata approach. + + Args: + file_path: Path to the source file + source: File content as bytes + + Returns: + Dict of function name -> signature string + """ + functions = self.extract_functions(None, file_path, source) + return { + f.name: self._format_signature(f) for f in functions + } + + def _format_signature(self, func: FunctionInfo) -> str: + """Format a FunctionInfo as a signature string.""" + params_str = ", ".join(func.parameters) + if func.return_type: + return f"{func.name}({params_str}) -> {func.return_type}" + return f"{func.name}({params_str})" diff --git a/src/codespy/tools/parsers/treesitter/extractors/ruby.py b/src/codespy/tools/parsers/treesitter/extractors/ruby.py new file mode 100644 index 0000000..30f6319 --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/ruby.py @@ -0,0 +1,132 @@ +"""Ruby function/method extractor.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor +from codespy.tools.parsers.treesitter.models import FunctionInfo + + +class RubyExtractor(BaseExtractor): + """Extract method definitions from Ruby source code.""" + + def extract_functions( + self, + node: Any, + file_path: Path, + source: bytes, + ) -> list[FunctionInfo]: + """Extract Ruby method definitions.""" + functions: list[FunctionInfo] = [] + + def visit(n: Any, in_class: bool = False) -> None: + if n.type == "method": + func_info = self._extract_method_info(n, file_path, source, in_class) + if func_info: + functions.append(func_info) + + elif n.type == "singleton_method": + func_info = self._extract_singleton_method_info(n, file_path, source) + if func_info: + functions.append(func_info) + + elif n.type == "lambda": + # Anonymous lambda - skip (no name) + pass + + elif n.type == "block": + # Block passed to a method - skip + pass + + for child in n.children: + # Track if we're inside a class/module + is_class = n.type in ("class", "module", "singleton_class") + visit(child, in_class or is_class) + + visit(node) + return functions + + def _extract_method_info( + self, + method_node: Any, + file_path: Path, + source: bytes, + in_class: bool, + ) -> FunctionInfo | None: + """Extract method info from a method node.""" + name_node = method_node.child_by_field_name("name") + if not name_node: + return None + + name = self._get_node_text(name_node, source) + params = self._extract_ruby_params(method_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=method_node.start_point[0] + 1, + line_end=method_node.end_point[0] + 1, + parameters=params, + return_type=None, # Ruby is dynamically typed + is_method=in_class, + ) + + def _extract_singleton_method_info( + self, + method_node: Any, + file_path: Path, + source: bytes, + ) -> FunctionInfo | None: + """Extract singleton method info (e.g., def self.method_name).""" + name_node = method_node.child_by_field_name("name") + if not name_node: + return None + + name = "self." + self._get_node_text(name_node, source) + params = self._extract_ruby_params(method_node, source) + + return FunctionInfo( + name=name, + file=str(file_path), + line_start=method_node.start_point[0] + 1, + line_end=method_node.end_point[0] + 1, + parameters=params, + return_type=None, + is_method=True, + ) + + def _extract_ruby_params(self, node: Any, source: bytes) -> list[str]: + """Extract Ruby method parameters.""" + params: list[str] = [] + params_node = node.child_by_field_name("parameters") + if not params_node: + return params + + for child in params_node.children: + # Ruby parameters can be: identifier, optional_parameter, keyword_parameter, etc. + if child.type in ("identifier", "simple_parameter"): + param_name = self._get_node_text(child, source) + if param_name and param_name not in ("(", ")", ",", "|", "&"): + params.append(param_name) + elif child.type in ("optional_parameter", "keyword_parameter"): + name_node = child.child_by_field_name("name") + if name_node: + param_name = self._get_node_text(name_node, source) + if param_name: + params.append(param_name) + elif child.type == "block_parameter": + # &block parameter + name_node = child.child_by_field_name("name") + if name_node: + param_name = "&" + self._get_node_text(name_node, source) + params.append(param_name) + elif child.type == "keyword_parameter": + name_node = child.child_by_field_name("name") + if name_node: + param_name = self._get_node_text(name_node, source) + if param_name: + params.append(f"{param_name}:") + + return params diff --git a/src/codespy/tools/parsers/treesitter/parser.py b/src/codespy/tools/parsers/treesitter/parser.py index 21695c8..894d44d 100644 --- a/src/codespy/tools/parsers/treesitter/parser.py +++ b/src/codespy/tools/parsers/treesitter/parser.py @@ -8,12 +8,17 @@ from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor from codespy.tools.parsers.treesitter.extractors import ( + BashExtractor, + CppExtractor, + CSharpExtractor, GoExtractor, JavaExtractor, JavaScriptExtractor, KotlinExtractor, ObjCExtractor, + PHPExtractor, PythonExtractor, + RubyExtractor, RustExtractor, SwiftExtractor, TerraformExtractor, @@ -41,6 +46,28 @@ from tree_sitter import Language, Node, Parser TREE_SITTER_AVAILABLE = True + + # Try to import new language grammars (optional) + try: + import tree_sitter_cpp as ts_cpp + except ImportError: + ts_cpp = None # type: ignore + try: + import tree_sitter_c_sharp as ts_csharp + except ImportError: + ts_csharp = None # type: ignore + try: + import tree_sitter_ruby as ts_ruby + except ImportError: + ts_ruby = None # type: ignore + try: + import tree_sitter_php as ts_php + except ImportError: + ts_php = None # type: ignore + try: + import tree_sitter_bash as ts_bash + except ImportError: + ts_bash = None # type: ignore except ImportError: TREE_SITTER_AVAILABLE = False Parser = Any # type: ignore[misc] @@ -75,6 +102,21 @@ class TreeSitterParser: "rs": ("rust", ["function_item"]), "tf": ("hcl", ["block"]), "tfvars": ("hcl", ["block"]), + # New languages + "c": ("cpp", ["function_definition"]), + "cpp": ("cpp", ["function_definition"]), + "cc": ("cpp", ["function_definition"]), + "cxx": ("cpp", ["function_definition"]), + "h": ("cpp", ["function_definition"]), + "hpp": ("cpp", ["function_definition"]), + "hh": ("cpp", ["function_definition"]), + "hxx": ("cpp", ["function_definition"]), + "cs": ("csharp", ["method_declaration", "constructor_declaration"]), + "rb": ("ruby", ["method", "singleton_method"]), + "php": ("php", ["function_definition", "method_declaration"]), + "sh": ("bash", ["function_definition"]), + "bash": ("bash", ["function_definition"]), + "zsh": ("bash", ["function_definition"]), } def __init__(self, repo_path: Path) -> None: @@ -111,6 +153,12 @@ def _init_extractors(self) -> None: "objc": ObjCExtractor(), "rust": RustExtractor(), "hcl": TerraformExtractor(), + # New languages + "cpp": CppExtractor(), + "csharp": CSharpExtractor(), + "ruby": RubyExtractor(), + "php": PHPExtractor(), + "bash": BashExtractor(), } def _init_languages(self) -> None: @@ -130,11 +178,20 @@ def _init_languages(self) -> None: ("objc", lambda: ts_objc.language()), ("rust", lambda: ts_rust.language()), ("hcl", lambda: ts_hcl.language()), + # New languages (conditional) + ("cpp", lambda: ts_cpp.language() if ts_cpp else None), + ("csharp", lambda: ts_csharp.language() if ts_csharp else None), + ("ruby", lambda: ts_ruby.language() if ts_ruby else None), + ("php", lambda: ts_php.language() if ts_php else None), + ("bash", lambda: ts_bash.language() if ts_bash else None), ] for lang_name, lang_func in language_configs: try: - self._languages[lang_name] = Language(lang_func()) + lang_result = lang_func() + if lang_result is None: + continue # Language grammar not available + self._languages[lang_name] = Language(lang_result) parser = Parser(self._languages[lang_name]) self._parsers[lang_name] = parser except Exception as e: From 56aa110bbb78b9fa187ddd32bd8e80fc8a5483b1 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 7 Aug 2026 23:54:17 +0200 Subject: [PATCH 33/79] treesitter upgrade --- .../parsers/treesitter/extractors/__init__.py | 2 + .../treesitter/extractors/regex_fallback.py | 411 ----------------- .../treesitter/extractors/ripgrep_fallback.py | 419 ++++++++++++++++++ 3 files changed, 421 insertions(+), 411 deletions(-) delete mode 100644 src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py create mode 100644 src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py diff --git a/src/codespy/tools/parsers/treesitter/extractors/__init__.py b/src/codespy/tools/parsers/treesitter/extractors/__init__.py index 40d5df1..342c30f 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/__init__.py +++ b/src/codespy/tools/parsers/treesitter/extractors/__init__.py @@ -10,6 +10,7 @@ from codespy.tools.parsers.treesitter.extractors.objc import ObjCExtractor from codespy.tools.parsers.treesitter.extractors.php import PHPExtractor from codespy.tools.parsers.treesitter.extractors.python import PythonExtractor +from codespy.tools.parsers.treesitter.extractors.ripgrep_fallback import RipgrepHeuristicsExtractor from codespy.tools.parsers.treesitter.extractors.ruby import RubyExtractor from codespy.tools.parsers.treesitter.extractors.rust import RustExtractor from codespy.tools.parsers.treesitter.extractors.swift import SwiftExtractor @@ -26,6 +27,7 @@ "ObjCExtractor", "PHPExtractor", "PythonExtractor", + "RipgrepHeuristicsExtractor", "RubyExtractor", "RustExtractor", "SwiftExtractor", diff --git a/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py b/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py deleted file mode 100644 index b7714f7..0000000 --- a/src/codespy/tools/parsers/treesitter/extractors/regex_fallback.py +++ /dev/null @@ -1,411 +0,0 @@ -"""Regex-based function extractor for languages without tree-sitter grammars. - -This module provides lightweight pattern matching as a fallback when tree-sitter -parsers are not available. It uses ripgrep for fast line-based searching combined -with heuristics to identify function definitions and extract signatures. -""" - -from __future__ import annotations - -import re -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -from codespy.tools.parsers.treesitter.base_extractor import BaseExtractor -from codespy.tools.parsers.treesitter.models import FunctionInfo - - -@dataclass -class LanguagePattern: - """Pattern configuration for a language.""" - - name: str - extensions: set[str] - # Pattern to match function definition line - function_pattern: re.Pattern - # Pattern to extract parameters from the definition line - param_pattern: re.Pattern | None = None - # Pattern to detect end of function (e.g., closing brace) - end_pattern: re.Pattern | None = None - # Comment characters to strip from signatures - comment_prefix: str | None = None - - -class RegexFallbackExtractor(BaseExtractor): - """Fallback extractor using regex patterns for unsupported languages. - - Supported languages: - - C/C++ (.c, .cpp, .h, .hpp) - - C# (.cs) - - Ruby (.rb) - - PHP (.php) - - Shell/Bash (.sh, .bash) - - SQL (.sql) - basic stored procedure detection - """ - - # Language patterns for function detection - PATTERNS: dict[str, LanguagePattern] = { - "c_cpp": LanguagePattern( - name="C/C++", - extensions={".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hh", ".hxx"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:static\s+|inline\s+|extern\s+|virtual\s+|explicit\s+|constexpr\s+|consteval\s+)*' # modifiers - r'(?:[\w:<>,\s\*&]+?\s+)?' # return type with templates/pointers - r'(\w+)' # function name (capture group 1) - r'\s*\([^)]*\)' # parameters - r'(?:\s*const)?' # optional const - r'(?:\s*->\s*[\w:<>,\s\*&]+)?' # optional trailing return (C++) - r'\s*[{;]', # opening brace or semicolon - re.MULTILINE, - ), - param_pattern=re.compile(r'\(([^)]*)\)'), - end_pattern=re.compile(r'^[\s]*}'), - comment_prefix="//", - ), - "csharp": LanguagePattern( - name="C#", - extensions={".cs"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|virtual\s+|' - r'override\s+|abstract\s+|sealed\s+|async\s+|unsafe\s+|extern\s+)*' # modifiers - r'(?:[\w<>,\s\[\]]+?\s+)' # return type (including generic/Task types) - r'(\w+)' # function name (capture group 1) - r'\s*\([^)]*\)' # parameters - r'(?:\s*where\s+\w+\s*:\s*[\w<>,\s]+)?' # optional generic constraint - r'\s*[{(]', # opening brace or expression body - re.MULTILINE, - ), - param_pattern=re.compile(r'\(([^)]*)\)'), - end_pattern=re.compile(r'^[\s]*[}]'), - comment_prefix="//", - ), - "ruby": LanguagePattern( - name="Ruby", - extensions={".rb", ".rbw", ".rake", ".gemspec"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:private\s+|protected\s+|public\s+)?' # visibility modifiers - r'def\s+' # def keyword - r'(?:self\.)?' # optional self. - r'(\w+[?!=]?)' # function name with optional ? ! = (capture group 1) - r'(?:\s*\([^)]*\))?' # optional parentheses with params - r'(?:\s+|$)', # whitespace or end of line - re.MULTILINE, - ), - # Ruby params are complex (block syntax, etc), keep simple - param_pattern=re.compile(r'def\s+(?:self\.)?\w+\s*\(([^)]*)\)'), - end_pattern=re.compile(r'^[\s]*end\s*$'), - comment_prefix="#", - ), - "php": LanguagePattern( - name="PHP", - extensions={".php", ".php4", ".php5", ".phtml"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:public\s+|private\s+|protected\s+)?' # visibility - r'(?:static\s+)?' # optional static - r'(?:abstract\s+|final\s+)?' # optional abstract/final - r'function\s+' # function keyword - r'(&)?' # optional reference return - r'(\w+)' # function name (capture group 2, 1 is &) - r'\s*\([^)]*\)', # parameters - re.MULTILINE, - ), - param_pattern=re.compile(r'\(([^)]*)\)'), - end_pattern=re.compile(r'^[\s]*}'), - comment_prefix="//", # Also supports # but // is more common - ), - "shell": LanguagePattern( - name="Shell/Bash", - extensions={".sh", ".bash", ".zsh", ".ksh", ".dash"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:function\s+)?' # optional function keyword - r'(\w+)' # function name (capture group 1) - r'\s*\(\s*\)' # empty parentheses - r'\s*\{', # opening brace - re.MULTILINE, - ), - # Shell functions typically don't declare params in signature - param_pattern=None, - end_pattern=re.compile(r'^[\s]*}'), - comment_prefix="#", - ), - "sql": LanguagePattern( - name="SQL", - extensions={".sql"}, - function_pattern=re.compile( - r'^[\s]*' # leading whitespace - r'(?:CREATE\s+OR\s+REPLACE\s+)?' # optional create or replace - r'(?:CREATE\s+)?' # optional create - r'(?:PROCEDURE|FUNCTION|TRIGGER|EVENT)\s+' # object type - r'(?:[\w.]+\s+)?' # optional schema prefix - r'(\w+)' # name (capture group 1) - r'\s*\(', # opening paren for params - re.MULTILINE | re.IGNORECASE, - ), - param_pattern=re.compile(r'\(([^)]*)\)'), - end_pattern=re.compile(r'^[\s]*END\s*;?', re.IGNORECASE), - comment_prefix="--", - ), - } - - def __init__(self) -> None: - """Initialize the extractor.""" - # Build extension -> pattern mapping for fast lookup - self._ext_to_pattern: dict[str, LanguagePattern] = {} - for pattern in self.PATTERNS.values(): - for ext in pattern.extensions: - self._ext_to_pattern[ext.lower()] = pattern - - def _get_pattern(self, file_path: Path) -> LanguagePattern | None: - """Get pattern for file based on extension.""" - ext = file_path.suffix.lower() - return self._ext_to_pattern.get(ext) - - def _strip_comments(self, line: str, comment_prefix: str | None) -> str: - """Remove inline comments from a line.""" - if not comment_prefix: - return line - # Handle both // and # style comments - if comment_prefix in line: - return line.split(comment_prefix)[0].rstrip() - return line - - def _extract_params(self, line: str, pattern: LanguagePattern) -> list[str]: - """Extract parameter names from function signature.""" - if not pattern.param_pattern: - return [] - - match = pattern.param_pattern.search(line) - if not match: - return [] - - params_str = match.group(1).strip() - if not params_str: - return [] - - # Simple splitting - handles common cases - # For complex cases (templates, function pointers), just return raw params - params = [] - current_param = "" - depth = 0 - - for char in params_str: - if char in "(<{": - depth += 1 - current_param += char - elif char in ")>}: - depth -= 1 - current_param += char - elif char == "," and depth == 0: - # End of parameter - param = current_param.strip() - if param: - # Extract parameter name (last word before any =) - param_name = self._extract_param_name(param) - if param_name: - params.append(param_name) - current_param = "" - else: - current_param += char - - # Handle last parameter - if current_param.strip(): - param_name = self._extract_param_name(current_param.strip()) - if param_name: - params.append(param_name) - - return params - - def _extract_param_name(self, param: str) -> str | None: - """Extract parameter name from parameter declaration. - - Examples: - - "int x" -> "x" - - "const std::string& name" -> "name" - - "int x = 5" -> "x" - - "std::vector items" -> "items" - """ - # Remove default values - if "=" in param: - param = param.split("=")[0].strip() - - # Split by whitespace and take last part - # Handle pointers/references by stripping * and & - parts = param.split() - if not parts: - return None - - name_part = parts[-1] - # Strip *, &, etc from the end - name = name_part.rstrip("*&").strip() - - # Validate it's a reasonable identifier - if re.match(r'^[a-zA-Z_]\w*$', name): - return name - return None - - def _find_function_end( - self, - lines: list[str], - start_line: int, - pattern: LanguagePattern, - ) -> int: - """Find the approximate end line of a function. - - Uses brace counting for C-style languages or end keyword detection. - """ - if not pattern.end_pattern and not pattern.function_pattern: - return start_line - - brace_depth = 0 - in_function = False - - for i, line in enumerate(lines[start_line - 1:], start=start_line): - stripped = line.strip() - - if not in_function: - # Look for opening brace to enter function - if "{" in line: - brace_depth = line.count("{") - line.count("}") - in_function = True - elif pattern.name in ("ruby", "sql") and stripped.startswith("def "): - in_function = True - continue - - # In function body - track braces - brace_depth += line.count("{") - line.count("}") - - # Check for end pattern - if pattern.end_pattern and pattern.end_pattern.match(line): - return i - - # For brace-based languages, depth reaching 0 means end - if pattern.name not in ("ruby", "sql") and brace_depth <= 0: - return i - - # Safety limit - don't search forever - if i - start_line > 500: - return start_line + 100 - - return min(start_line + 50, len(lines)) - - def extract_functions( - self, - root_node: Any, # Not used, for compatibility with BaseExtractor - file_path: Path, - source: bytes, - ) -> list[FunctionInfo]: - """Extract function definitions using regex patterns. - - Args: - root_node: Not used (for compatibility) - file_path: Path to the source file - source: File content as bytes - - Returns: - List of FunctionInfo objects - """ - pattern = self._get_pattern(file_path) - if not pattern: - return [] - - try: - content = source.decode("utf-8", errors="ignore") - except Exception: - return [] - - lines = content.split("\n") - functions = [] - seen_lines: set[int] = set() # Track to avoid duplicates - - for match in pattern.function_pattern.finditer(content): - name = match.group(1) - if not name: - continue - - # Calculate line number - line_start = content[: match.start()].count("\n") + 1 - - # Skip if we've seen this line (can happen with overlapping patterns) - if line_start in seen_lines: - continue - seen_lines.add(line_start) - - # Get the full line for parameter extraction - line_idx = line_start - 1 - if line_idx >= len(lines): - continue - - line = lines[line_idx] - clean_line = self._strip_comments(line, pattern.comment_prefix) - - # Extract parameters - params = self._extract_params(clean_line, pattern) - - # Find approximate end line - line_end = self._find_function_end(lines, line_start, pattern) - - # Determine return type (heuristic) - return_type = None - if pattern.name in ("c_cpp", "csharp"): - # Try to extract return type from before function name - func_match = pattern.function_pattern.match(clean_line) - if func_match: - prefix = clean_line[: func_match.start(1)].strip() - # Remove modifiers - for mod in ["static", "inline", "extern", "virtual", "explicit", - "constexpr", "consteval", "public", "private", - "protected", "internal", "async", "abstract", - "sealed", "unsafe", "override"]: - prefix = re.sub(rf"\b{mod}\b\s*", "", prefix) - return_type = prefix.strip() if prefix.strip() else None - - functions.append( - FunctionInfo( - name=name, - file=str(file_path), - line_start=line_start, - line_end=line_end, - parameters=params, - return_type=return_type, - is_method=False, # Could be refined - receiver_type=None, - docstring=None, - ) - ) - - return functions - - def extract_signatures( - self, - file_path: Path, - source: bytes, - ) -> dict[str, str]: - """Extract function signatures as strings. - - Returns a mapping of function name -> signature string - for use in hunks+metadata approach. - - Args: - file_path: Path to the source file - source: File content as bytes - - Returns: - Dict of function name -> signature string - """ - functions = self.extract_functions(None, file_path, source) - return { - f.name: self._format_signature(f) for f in functions - } - - def _format_signature(self, func: FunctionInfo) -> str: - """Format a FunctionInfo as a signature string.""" - params_str = ", ".join(func.parameters) - if func.return_type: - return f"{func.name}({params_str}) -> {func.return_type}" - return f"{func.name}({params_str})" diff --git a/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py new file mode 100644 index 0000000..11e258e --- /dev/null +++ b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py @@ -0,0 +1,419 @@ +"""Ripgrep + heuristics fallback extractor for function detection. + +This module provides a generic fallback for extracting function information +when tree-sitter is not available. It uses ripgrep to find function definition +lines and interval intersection to determine which functions are affected by +changed lines. + +Algorithm: + 1. Run ripgrep on the file with generic definition patterns + → sorted list of (line_number, function_name) + + 2. Derive implicit boundaries: each function spans from its definition + line to the line before the next definition (or EOF) + + 3. Intersect these boundaries with changed_line_ranges + → return functions that contain at least one changed line + +This approach handles all cases elegantly: + - Body changes: Changed lines fall within a function's derived range + - New function: The definition line IS a changed line, matches itself + - Signature changes: Definition line is changed, matches itself + - Multiple functions: Multiple intersections found +""" + +from __future__ import annotations + +import logging +import re +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from codespy.tools.parsers.treesitter.models import FunctionInfo + +logger = logging.getLogger(__name__) + + +class RipgrepHeuristicsExtractor: + """Generic fallback extractor using ripgrep + interval intersection. + + Instead of parsing the entire file with complex AST analysis, this extractor: + 1. Uses ripgrep to find all function definition lines in the file + 2. Derives implicit boundaries: each function spans to the next definition + 3. Intersects these ranges with the changed line ranges from the patch + + This is much faster than AST parsing and handles all edge cases: + - Body changes: Lines fall within derived range + - New functions: Definition line IS a changed line + - Modified signatures: Definition line is changed + """ + + # Generic patterns that match function definitions across languages. + # Ordered by specificity - more specific patterns first. + DEFINITION_PATTERNS: list[tuple[str, re.Pattern]] = [ + # Keyword-based: def, func, fn, fun, function, sub, proc + # Matches: Python, Ruby, Go, JavaScript, TypeScript, Rust, etc. + ( + "keyword", + re.compile( + r"^[\s]*(?:" # leading whitespace + r"(?:pub|priv|protected|private|public|export|async|static|inline|const|final)\s+)*" # modifiers + r"(?:def|func|fn|fun|function|sub|proc)\s+" # keyword + r"(?:self\.)?" # optional self. for Ruby + r"(\w+)" # function name (capture group 1) + ), + ), + # C-style: return_type name(params) { + # Matches: C, C++, C#, Java, PHP, etc. + ( + "c_style", + re.compile( + r"^[\s]*" # leading whitespace + r"(?:[\w\*&<>,:\s]+\s+)" # return type with modifiers/pointers + r"(\w+)" # function name (capture group 1) + r"\s*\([^;]*$" # opening paren, avoid forward declarations ending in ; + ), + ), + # Shell/Bash: function name() { or name() { + ( + "shell", + re.compile( + r"^[\s]*" # leading whitespace + r"(?:function\s+)?" # optional 'function' keyword + r"(\w+)" # function name + r"\s*\(\s*\)" # empty parentheses + ), + ), + # SQL: CREATE FUNCTION/PROCEDURE/TRIGGER name + ( + "sql", + re.compile( + r"^[\s]*" # leading whitespace + r"(?:CREATE\s+(?:OR\s+REPLACE\s+)?)?" # optional CREATE OR REPLACE + r"(?:PROCEDURE|FUNCTION|TRIGGER)\s+" # object type + r"(?:[\w.]+\s+)?" # optional schema prefix + r"(\w+)", # name + re.IGNORECASE, + ), + ), + ] + + # Map file extensions to preferred pattern order + EXTENSION_PRIORITY: dict[str, list[str]] = { + ".py": ["keyword"], # Python uses 'def' + ".rb": ["keyword"], # Ruby uses 'def' + ".go": ["keyword"], # Go uses 'func' + ".rs": ["keyword"], # Rust uses 'fn' + ".js": ["keyword", "c_style"], # JavaScript can use both + ".ts": ["keyword", "c_style"], # TypeScript + ".c": ["c_style"], # C uses c_style + ".cpp": ["c_style"], # C++ + ".cc": ["c_style"], + ".cxx": ["c_style"], + ".h": ["c_style"], + ".hpp": ["c_style"], + ".cs": ["c_style"], # C# + ".java": ["c_style"], # Java + ".php": ["c_style", "keyword"], # PHP uses both + ".sh": ["shell", "keyword"], # Shell + ".bash": ["shell", "keyword"], + ".zsh": ["shell", "keyword"], + ".sql": ["sql"], # SQL + } + + def __init__(self, repo_path: Path) -> None: + """Initialize the extractor. + + Args: + repo_path: Path to the repository root (for context) + """ + self.repo_path = repo_path + self._rg_available = shutil.which("rg") is not None + + def _get_patterns_for_file(self, file_path: Path) -> list[tuple[str, re.Pattern]]: + """Get definition patterns ordered by priority for the file extension.""" + ext = file_path.suffix.lower() + priority_order = self.EXTENSION_PRIORITY.get(ext, []) + + # Build ordered list based on priority + ordered = [] + for pattern_name in priority_order: + for name, pattern in self.DEFINITION_PATTERNS: + if name == pattern_name and (name, pattern) not in ordered: + ordered.append((name, pattern)) + + # Add remaining patterns not in priority list + for name, pattern in self.DEFINITION_PATTERNS: + if (name, pattern) not in ordered: + ordered.append((name, pattern)) + + return ordered + + def _find_definitions( + self, + file_path: Path, + ) -> list[tuple[int, str, str]]: + """Find all function definition lines in a file using ripgrep. + + Args: + file_path: Path to the source file + + Returns: + List of (line_number, function_name, full_line) tuples, sorted by line_number + """ + if not self._rg_available: + logger.debug("ripgrep not available, skipping definition search") + return [] + + patterns = self._get_patterns_for_file(file_path) + if not patterns: + return [] + + # Combine patterns with alternation for single ripgrep call + # Extract just the pattern regexes + pattern_regexes = [p[1].pattern for p in patterns] + combined_pattern = "|".join(f"({p})" for p in pattern_regexes) + + definitions: list[tuple[int, str, str]] = [] + + try: + cmd = [ + "rg", + "--line-number", + "--no-heading", + "--with-filename", + "--color=never", + "--multiline", # Handle multi-line patterns + combined_pattern, + str(file_path), + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=30, + cwd=str(self.repo_path), + ) + + # ripgrep returns 1 when no matches found (not an error) + if result.returncode > 1: + logger.debug(f"ripgrep search failed: {result.stderr}") + return [] + + # Parse results: file:line:content + for line in result.stdout.strip().split("\n"): + if not line: + continue + + # Parse: filepath:line:content + # Handle Windows/Unix path differences + parts = line.split(":", 2) + if len(parts) < 3: + continue + + try: + line_num = int(parts[1]) + except ValueError: + continue + + content = parts[2] + + # Try each pattern to extract function name + for pattern_name, pattern in patterns: + match = pattern.match(content) + if match: + func_name = match.group(1) if match.lastindex else None + if func_name: + definitions.append((line_num, func_name, content.strip())) + break # Found a match, move to next line + + except subprocess.TimeoutExpired: + logger.warning(f"ripgrep search timed out for {file_path}") + except Exception as e: + logger.debug(f"ripgrep search failed for {file_path}: {e}") + + # Sort by line number and remove duplicates + seen = set() + unique_defs = [] + for line_num, func_name, full_line in sorted(definitions): + key = (line_num, func_name) + if key not in seen: + seen.add(key) + unique_defs.append((line_num, func_name, full_line)) + + return unique_defs + + def _derive_boundaries( + self, + definitions: list[tuple[int, str, str]], + total_lines: int, + ) -> list[tuple[int, int, str, str]]: + """Derive function boundaries from definition lines. + + Each function spans from its definition line to the line before + the next function's definition (or EOF). + + Args: + definitions: List of (line_number, function_name, full_line) + total_lines: Total number of lines in the file + + Returns: + List of (start_line, end_line, function_name, signature_line) + """ + boundaries = [] + for i, (line_num, func_name, full_line) in enumerate(definitions): + # End is line before next definition, or EOF + if i + 1 < len(definitions): + end_line = definitions[i + 1][0] - 1 + else: + end_line = total_lines + + boundaries.append((line_num, end_line, func_name, full_line)) + + return boundaries + + def _count_file_lines(self, file_path: Path) -> int: + """Count total lines in a file efficiently.""" + try: + # Use wc -l for efficiency on large files + result = subprocess.run( + ["wc", "-l", str(file_path)], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + # Parse: " 123 filename" + parts = result.stdout.strip().split() + if parts: + return int(parts[0]) + except Exception: + pass + + # Fallback: read and count + try: + with open(file_path, "rb") as f: + return sum(1 for _ in f) + except Exception as e: + logger.debug(f"Failed to count lines in {file_path}: {e}") + return 0 + + def _extract_parameters(self, signature_line: str) -> list[str]: + """Extract parameter names from a function signature line. + + Best-effort extraction for common patterns. + """ + params: list[str] = [] + + # Look for parentheses with content + match = re.search(r"\(([^)]*)\)", signature_line) + if not match: + return params + + params_str = match.group(1).strip() + if not params_str: + return params + + # Split by comma, handle simple cases + for param in params_str.split(","): + param = param.strip() + if not param: + continue + + # Extract parameter name (last word before = or end) + # Examples: "int x", "const string& name", "x int" + param = param.split("=")[0].strip() # Remove default values + words = param.split() + if words: + # Last word is usually the parameter name + name = words[-1].rstrip("*&") + if re.match(r"^[a-zA-Z_]\w*$", name): + params.append(name) + + return params + + def extract_functions( + self, + file_path: Path, + changed_line_ranges: list[tuple[int, int]], + ) -> list[FunctionInfo]: + """Extract functions affected by the changed line ranges. + + Args: + file_path: Path to the source file + changed_line_ranges: List of (start, end) tuples for changed lines + + Returns: + List of FunctionInfo for functions that overlap with changed lines + """ + if not file_path.exists(): + logger.debug(f"File not found: {file_path}") + return [] + + if not changed_line_ranges: + logger.debug("No changed line ranges provided") + return [] + + # Step 1: Find all function definitions + definitions = self._find_definitions(file_path) + if not definitions: + logger.debug(f"No function definitions found in {file_path}") + return [] + + # Step 2: Get total lines and derive boundaries + total_lines = self._count_file_lines(file_path) + boundaries = self._derive_boundaries(definitions, total_lines) + + # Step 3: Intersect with changed ranges + affected_functions: list[FunctionInfo] = [] + seen_names: set[str] = set() + + for func_start, func_end, func_name, signature_line in boundaries: + # Check if this function overlaps with any changed range + overlaps = False + for change_start, change_end in changed_line_ranges: + # Overlap condition: func_start <= change_end AND func_end >= change_start + if func_start <= change_end and func_end >= change_start: + overlaps = True + break + + if overlaps and func_name not in seen_names: + params = self._extract_parameters(signature_line) + + # Build return_type from signature (best effort) + return_type = None + # Try to extract return type from C-style signatures + c_match = re.match( + r"^[\s]*([\w\*&<>,:\s]+)\s+\w+\s*\(", signature_line + ) + if c_match: + potential = c_match.group(1).strip() + # Filter out modifiers + modifiers = {"static", "inline", "extern", "virtual", "const", "async", "public", "private", "protected"} + words = potential.split() + filtered = [w for w in words if w not in modifiers] + if filtered: + return_type = " ".join(filtered) + + affected_functions.append( + FunctionInfo( + name=func_name, + file=str(file_path), + line_start=func_start, + line_end=func_end, + parameters=params, + return_type=return_type, + is_method=False, # Cannot determine from single line + receiver_type=None, + docstring=None, + ) + ) + seen_names.add(func_name) + + logger.debug( + f"Found {len(affected_functions)} affected functions in {file_path}" + ) + return affected_functions From 3f4929aff06e4ec745c10c9037cc6e35435b4fd6 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sat, 8 Aug 2026 20:30:58 +0200 Subject: [PATCH 34/79] wip --- src/codespy/agents/reviewer/modules/doc_reviewer.py | 2 +- src/codespy/agents/reviewer/modules/scope_identifier.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 59bbfd3..d6bc13f 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -106,7 +106,7 @@ def __init__(self) -> None: self._settings = get_settings() def _build_patches(self, scope: ScopeResult) -> str: - """Build compact patches representation.""" + """Build patches representation for review.""" parts: list[str] = [] for f in scope.changed_files: if f.patch: diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index ed900e0..6bcfde8 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -265,7 +265,7 @@ async def aforward( if not reviewable_files: logger.warning("No reviewable files in MR - all files are binary, lock files, or in excluded directories") - return [] + return [], review_context.memory if review_context else None # Repo identifier for this review, stamped onto every ScopeResult # (used by Hippocampus memory to build the episode path). Uses the From e7d7e2884054d6e68853497b666dd9e9690509dd Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sat, 8 Aug 2026 23:25:29 +0200 Subject: [PATCH 35/79] wip --- codespy.yaml | 4 +- src/codespy/agents/reviewer/reviewer.py | 5 + src/codespy/config.py | 2 +- src/codespy/tools/git/patch_utils.py | 447 ++++++++++++++++++ tests/test_patch_utils.py | 592 ++++++++++++++++++++++++ 5 files changed, 1047 insertions(+), 3 deletions(-) create mode 100644 src/codespy/tools/git/patch_utils.py create mode 100644 tests/test_patch_utils.py diff --git a/codespy.yaml b/codespy.yaml index b211dd3..451ca34 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -172,7 +172,7 @@ memory: # These apply to all signatures and reflection modules unless overridden default_model: anthropic/claude-opus-4-6 # DEFAULT_MODEL extraction_model: null # EXTRACTION_MODEL (falls back to default_model) -default_max_iters: 20 # DEFAULT_MAX_ITERS +default_max_iters: 5 # DEFAULT_MAX_ITERS default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) default_temperature: 1 # DEFAULT_TEMPERATURE (must be 1 while reasoning is enabled) @@ -248,7 +248,7 @@ signatures: # Scope Identifier signature scope: enabled: true # SCOPE_ENABLED - max_iters: null # SCOPE_MAX_ITERS + max_iters: 20 # SCOPE_MAX_ITERS model: null # SCOPE_MODEL reasoning_effort: null # SCOPE_REASONING_EFFORT temperature: null # SCOPE_TEMPERATURE diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 33c4938..ad9e5e7 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -12,6 +12,7 @@ from codespy.config import Settings, get_settings from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff +from codespy.tools.git.patch_utils import compact_patches from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import ( Issue, @@ -216,6 +217,10 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Update ReviewContext with Scope Identifier's memory for downstream modules review_ctx = ReviewContext(pr_context=pr_context, memory=scope_memory) + # Compact patches: expand context to function bodies for better review context + logger.info("Compacting patches to function boundaries...") + compact_patches(scopes, repo_path) + # Step 3: Run review modules concurrently via asyncio.gather (inherit Scope Identifier memory) module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") diff --git a/src/codespy/config.py b/src/codespy/config.py index 43edc59..a20c963 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -124,7 +124,7 @@ class Settings(BaseSettings): # Top-level defaults (also available via env vars DEFAULT_MODEL, etc.) default_model: str = "anthropic/claude-opus-4-6" extraction_model: str | None = None # TwoStepAdapter extraction (falls back to default_model) - default_max_iters: int = 3 + default_max_iters: int = 5 # Provider reasoning budget; LiteLLM maps this to each provider's native parameter. default_reasoning_effort: ReasoningEffort = "medium" # Providers require temperature=1 when reasoning is enabled. diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py new file mode 100644 index 0000000..afa0e16 --- /dev/null +++ b/src/codespy/tools/git/patch_utils.py @@ -0,0 +1,447 @@ +"""Patch compaction utilities to expand diff context to function boundaries.""" + +from __future__ import annotations + +import logging +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from codespy.agents.reviewer.models import ScopeResult + +from codespy.tools.git.models import ChangedFile, FileStatus +from codespy.tools.parsers.treesitter import FunctionInfo, TreeSitterParser + +logger = logging.getLogger(__name__) + +# Non-code file extensions that should not be processed +NON_CODE_EXTENSIONS = { + "md", + "txt", + "rst", + "yaml", + "yml", + "json", + "toml", + "ini", + "cfg", + "conf", + "xml", + "html", + "htm", + "css", + "scss", + "sass", + "less", + "csv", + "tsv", +} + +# Compiled regex for hunk header parsing +HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@") + + +def compact_patches(scopes: list[ScopeResult], repo_path: Path) -> None: + """Compact patches on all changed files across scopes (mutates in place). + + For each code file with a patch, attempts compaction. Non-code files + and files where compaction fails keep their original patch. + + Args: + scopes: List of scope results containing changed files + repo_path: Path to the cloned repository + """ + parser = TreeSitterParser(repo_path) + + for scope in scopes: + for file in scope.changed_files: + if not _should_compact_file(file): + continue + + try: + if file.patch is None: + continue + compacted = compact_patch(file.patch, Path(file.filename), repo_path, parser) + if compacted != file.patch: + logger.debug(f"Compacted patch for {file.filename}") + file.patch = compacted + except Exception as e: + logger.debug(f"Failed to compact patch for {file.filename}: {e}") + # Keep original patch on failure + + +def _should_compact_file(file: ChangedFile) -> bool: + """Check if a file should be compacted. + + Args: + file: The ChangedFile to check + + Returns: + True if the file should be compacted + """ + # Skip files without patches + if not file.patch: + return False + + # Skip deleted files (no source to read) + if file.status == FileStatus.REMOVED: + return False + + # Skip non-code files + if file.extension in NON_CODE_EXTENSIONS: + return False + + # Skip binary and lock files + if file.is_binary or file.is_lock_file: + return False + + return True + + +def compact_patch( + raw_patch: str, + file_path: Path, + repo_path: Path, + parser: TreeSitterParser | None = None, +) -> str: + """Expand diff context to full function bodies using TreeSitter. + + Returns compacted patch string, or raw_patch unchanged if compaction + isn't possible (no TreeSitter, not a code file, deleted file, etc.). + + Args: + raw_patch: The original unified diff patch + file_path: Path to the file (relative to repo root) + repo_path: Path to the repository root + parser: Optional TreeSitterParser instance (created if not provided) + + Returns: + Compacted patch or original patch if compaction fails + """ + # Skip empty patches + if not raw_patch: + return raw_patch + + # Create parser if not provided + if parser is None: + parser = TreeSitterParser(repo_path) + + # Check if TreeSitter is available + if not parser.available: + logger.debug(f"TreeSitter not available, skipping compaction for {file_path}") + return raw_patch + + # Check if file is a code file we can parse + extension = file_path.suffix.lstrip(".") if file_path.suffix else "" + if not extension or extension in NON_CODE_EXTENSIONS: + return raw_patch + + # Get absolute file path + abs_file_path = repo_path / file_path + if not abs_file_path.exists(): + logger.warning(f"Source file not found: {abs_file_path}") + return raw_patch + + # Find function definitions in the file + try: + functions = parser.find_function_definitions(abs_file_path) + except Exception as e: + logger.debug(f"Failed to find functions in {file_path}: {e}") + return raw_patch + + if not functions: + # No functions found, nothing to expand + return raw_patch + + # Parse hunks from patch + hunks = _parse_hunks(raw_patch) + if not hunks: + return raw_patch + + # Read source file lines + try: + source_lines = abs_file_path.read_text().splitlines() + except Exception as e: + logger.debug(f"Failed to read source file {file_path}: {e}") + return raw_patch + + # Expand each hunk to function boundaries + expanded_hunks: list[dict[str, Any]] = [] + for hunk in hunks: + expanded = _expand_hunk_to_functions(hunk, functions, source_lines) + if expanded: + expanded_hunks.append(expanded) + + if not expanded_hunks: + return raw_patch + + # Merge overlapping hunks + merged_hunks = _merge_hunks(expanded_hunks) + + # Rebuild the compacted patch + compacted = _rebuild_patch(raw_patch, merged_hunks, source_lines) + + return compacted if compacted else raw_patch + + +def _parse_hunks(patch: str) -> list[dict[str, Any]]: + """Parse hunks from a unified diff patch. + + Args: + patch: The unified diff patch string + + Returns: + List of hunk dictionaries with parsed info + """ + hunks = [] + lines = patch.split("\n") + i = 0 + + while i < len(lines): + line = lines[i] + + # Check for hunk header + match = HUNK_HEADER_RE.match(line) + if match: + old_start = int(match.group(1)) + old_count = int(match.group(2)) if match.group(2) else 1 + new_start = int(match.group(3)) + new_count = int(match.group(4)) if match.group(4) else 1 + + # Collect hunk lines + hunk_lines = [] + i += 1 + changed_new_lines = [] + current_new_line = new_start + + while i < len(lines): + hunk_line = lines[i] + + # Stop at next hunk header or empty line that ends the hunk + if hunk_line.startswith("@@"): + break + + hunk_lines.append(hunk_line) + + # Track changed lines in the new file + if hunk_line.startswith("+"): + changed_new_lines.append(current_new_line) + current_new_line += 1 + elif hunk_line.startswith(" "): + current_new_line += 1 + elif hunk_line.startswith("-"): + pass # Deleted line, not in new file + elif hunk_line.startswith("\\"): + # "\ No newline at end of file" marker + pass + + i += 1 + + hunks.append({ + "header": line, + "old_start": old_start, + "old_count": old_count, + "new_start": new_start, + "new_count": new_count, + "lines": hunk_lines, + "changed_new_lines": changed_new_lines, + }) + else: + i += 1 + + return hunks + + +def _expand_hunk_to_functions( + hunk: dict[str, Any], + functions: list[FunctionInfo], + source_lines: list[str], +) -> dict[str, Any] | None: + """Expand a hunk to cover enclosing function boundaries. + + Args: + hunk: The hunk dictionary + functions: List of function definitions + source_lines: Source file lines + + Returns: + Expanded hunk dictionary or None if no expansion needed + """ + changed_lines = hunk["changed_new_lines"] + if not changed_lines: + return hunk + + # Find enclosing functions for changed lines + enclosing_functions: list[FunctionInfo] = [] + for line_num in changed_lines: + # Find innermost enclosing function + best_match: FunctionInfo | None = None + for func in functions: + if func.line_start <= line_num <= func.line_end: + if best_match is None or ( + func.line_start >= best_match.line_start + and func.line_end <= best_match.line_end + ): + best_match = func + + if best_match and best_match not in enclosing_functions: + enclosing_functions.append(best_match) + + if not enclosing_functions: + # No enclosing functions, return hunk unchanged + return hunk + + # Calculate expansion range + min_func_start = min(f.line_start for f in enclosing_functions) + max_func_end = max(f.line_end for f in enclosing_functions) + + # Determine hunk boundaries in new file + hunk_start_new = hunk["new_start"] + hunk_end_new = hunk_start_new + hunk["new_count"] - 1 + # Account for lines that don't end with newline + if hunk["lines"] and hunk["lines"][-1].startswith("\\"): + hunk_end_new = hunk_start_new + hunk["new_count"] + + # Don't expand if already covering the entire function(s) + if min_func_start >= hunk_start_new and max_func_end <= hunk_end_new: + return hunk + + # Calculate expansion + expansion_start = min(hunk_start_new, min_func_start) + expansion_end = max(hunk_end_new, max_func_end) + + return { + "original_hunk": hunk, + "expansion_start": expansion_start, + "expansion_end": expansion_end, + "hunk_start_new": hunk_start_new, + "hunk_end_new": hunk_end_new, + "enclosing_functions": enclosing_functions, + } + + +def _merge_hunks(expanded_hunks: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Merge overlapping or adjacent expanded hunks. + + Args: + expanded_hunks: List of expanded hunk dictionaries + + Returns: + List of merged hunks + """ + if len(expanded_hunks) <= 1: + return expanded_hunks + + # Sort by expansion start + sorted_hunks = sorted(expanded_hunks, key=lambda h: h.get("expansion_start", 0)) + + merged = [sorted_hunks[0]] + + for hunk in sorted_hunks[1:]: + last = merged[-1] + + # Check if this hunk overlaps or is adjacent to the last merged hunk + last_end = last.get("expansion_end", 0) + current_start = hunk.get("expansion_start", 0) + + if current_start <= last_end + 1: # Overlapping or adjacent + # Merge: extend the end if needed + last["expansion_end"] = max(last_end, hunk.get("expansion_end", 0)) + # Keep track of original hunks for reconstruction + if "original_hunk" in last and "original_hunk" in hunk: + if "merged_hunks" not in last: + last["merged_hunks"] = [last["original_hunk"]] + last["merged_hunks"].append(hunk["original_hunk"]) + else: + merged.append(hunk) + + return merged + + +def _rebuild_patch( + raw_patch: str, + merged_hunks: list[dict[str, Any]], + source_lines: list[str], +) -> str | None: + """Rebuild the patch with expanded context. + + Args: + raw_patch: The original patch + merged_hunks: List of merged expanded hunks + source_lines: Source file lines + + Returns: + Rebuilt patch string or None if rebuild fails + """ + if not merged_hunks: + return None + + # Split original patch to get header lines (before first hunk) + lines = raw_patch.split("\n") + header_lines = [] + for line in lines: + if line.startswith("@@"): + break + header_lines.append(line) + + result_lines = list(header_lines) + + for merged_hunk in merged_hunks: + expansion_start = merged_hunk.get("expansion_start") + expansion_end = merged_hunk.get("expansion_end") + original_hunk = merged_hunk.get("original_hunk", merged_hunk) + + if expansion_start is None or expansion_end is None: + # No expansion, keep original hunk + result_lines.append(original_hunk["header"]) + result_lines.extend(original_hunk["lines"]) + continue + + # Get the hunk boundaries + hunk_start_new = merged_hunk.get("hunk_start_new", expansion_start) + hunk_end_new = merged_hunk.get("hunk_end_new", expansion_end) + + # Build new hunk lines + new_hunk_lines = [] + + # Add context lines before the original hunk (from expansion_start to hunk_start_new - 1) + for line_num in range(expansion_start, hunk_start_new): + if line_num <= len(source_lines): + new_hunk_lines.append(f" {source_lines[line_num - 1]}") + + # Add original hunk lines (excluding context lines that are now part of expansion) + original_lines = original_hunk.get("lines", []) + for line in original_lines: + if line.startswith(" "): + # Check if this context line is within our expansion range + # We already added the expansion context, so skip original context + # that overlaps with expansion + pass + else: + new_hunk_lines.append(line) + + # Add context lines after the original hunk (from hunk_end_new + 1 to expansion_end) + for line_num in range(hunk_end_new + 1, expansion_end + 1): + if line_num <= len(source_lines): + new_hunk_lines.append(f" {source_lines[line_num - 1]}") + + # Calculate new hunk header counts + # For the new file: count context lines and additions + new_file_count = 0 + for line in new_hunk_lines: + if line.startswith(" ") or line.startswith("+"): + new_file_count += 1 + + # For the old file: we approximate by using the ratio of original change + # This is a simplification; the old file line numbers would need full reconstruction + # We use the original old_count as a reasonable approximation + old_count = original_hunk.get("old_count", new_file_count) + + # Build new header + new_header = f"@@ -{expansion_start},{old_count} +{expansion_start},{new_file_count} @@" + + result_lines.append(new_header) + result_lines.extend(new_hunk_lines) + + return "\n".join(result_lines) diff --git a/tests/test_patch_utils.py b/tests/test_patch_utils.py new file mode 100644 index 0000000..df6c9af --- /dev/null +++ b/tests/test_patch_utils.py @@ -0,0 +1,592 @@ +"""Tests for patch_utils module.""" + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from codespy.agents.reviewer.models import ScopeResult, ScopeType +from codespy.tools.git.models import ChangedFile, FileStatus +from codespy.tools.git.patch_utils import ( + _expand_hunk_to_functions, + _merge_hunks, + _parse_hunks, + _rebuild_patch, + _should_compact_file, + compact_patch, + compact_patches, +) +from codespy.tools.parsers.treesitter import FunctionInfo + + +class TestShouldCompactFile: + """Tests for _should_compact_file function.""" + + def test_returns_true_for_code_file_with_patch(self): + file = ChangedFile( + filename="src/main.py", + status=FileStatus.MODIFIED, + patch="@@ -1,3 +1,3 @@\n-old\n+new", + ) + assert _should_compact_file(file) is True + + def test_returns_false_for_no_patch(self): + file = ChangedFile( + filename="src/main.py", + status=FileStatus.MODIFIED, + patch=None, + ) + assert _should_compact_file(file) is False + + def test_returns_false_for_deleted_file(self): + file = ChangedFile( + filename="src/main.py", + status=FileStatus.REMOVED, + patch="@@ -1,3 +0,0 @@\n-old", + ) + assert _should_compact_file(file) is False + + def test_returns_false_for_non_code_file(self): + file = ChangedFile( + filename="README.md", + status=FileStatus.MODIFIED, + patch="@@ -1,3 +1,3 @@\n-old\n+new", + ) + assert _should_compact_file(file) is False + + def test_returns_false_for_binary_file(self): + file = ChangedFile( + filename="image.png", + status=FileStatus.ADDED, + patch=None, + ) + assert _should_compact_file(file) is False + + def test_returns_false_for_lock_file(self): + file = ChangedFile( + filename="package-lock.json", + status=FileStatus.MODIFIED, + patch="@@ -1,3 +1,3 @@\n-old\n+new", + ) + assert _should_compact_file(file) is False + + +class TestParseHunks: + """Tests for _parse_hunks function.""" + + def test_parses_single_hunk(self): + patch = """@@ -1,5 +1,5 @@ + def hello(): +- print("old") ++ print("new") + pass""" + hunks = _parse_hunks(patch) + + assert len(hunks) == 1 + assert hunks[0]["old_start"] == 1 + assert hunks[0]["old_count"] == 5 + assert hunks[0]["new_start"] == 1 + assert hunks[0]["new_count"] == 5 + assert len(hunks[0]["lines"]) == 4 # Includes the context line + + def test_parses_multiple_hunks(self): + patch = """@@ -1,3 +1,3 @@ + line1 +-line2 ++line2_changed + line3 +@@ -10,3 +10,3 @@ + line10 +-line11 ++line11_changed + line12""" + hunks = _parse_hunks(patch) + + assert len(hunks) == 2 + assert hunks[0]["new_start"] == 1 + assert hunks[1]["new_start"] == 10 + + def test_handles_hunk_without_old_count(self): + patch = """@@ -1 +1 @@ +-old ++new""" + hunks = _parse_hunks(patch) + + assert len(hunks) == 1 + assert hunks[0]["old_count"] == 1 + assert hunks[0]["new_count"] == 1 + + def test_tracks_changed_new_lines(self): + patch = """@@ -5,5 +5,6 @@ + context1 +-added1 + context2 ++added2 + context3""" + hunks = _parse_hunks(patch) + + # Changed lines in new file: line 7 (added2) - deleted lines aren't in new file + # Line 5: context1 (line 5), Line 6: deleted (not in new), Line 6: context2 (line 6) + # Line 7: added2 (line 7) + assert hunks[0]["changed_new_lines"] == [7] + + def test_returns_empty_list_for_no_hunks(self): + patch = "no hunk headers here" + hunks = _parse_hunks(patch) + assert hunks == [] + + def test_handles_no_newline_at_end_marker(self): + patch = """@@ -1,3 +1,3 @@ + line1 + line2 +-line3 ++line3_new +\\ No newline at end of file""" + hunks = _parse_hunks(patch) + + assert len(hunks) == 1 + # The "\ No newline" line is included in hunk lines + # Lines: line1 (context), -line3 (deleted), +line3_new (added), \ marker + assert len(hunks[0]["lines"]) == 5 + + +class TestExpandHunkToFunctions: + """Tests for _expand_hunk_to_functions function.""" + + def test_expands_to_enclosing_function(self): + hunk = { + "header": "@@ -17,7 +17,7 @@", + "old_start": 17, + "old_count": 7, + "new_start": 17, + "new_count": 7, + "lines": [" line1", "- old_line", "+ new_line", " line2"], + "changed_new_lines": [18], + } + # Function from line 10 to 30 + functions = [FunctionInfo( + name="test_func", + file="test.py", + line_start=10, + line_end=30, + parameters=[], + )] + source_lines = [f"line {i}" for i in range(1, 35)] + + result = _expand_hunk_to_functions(hunk, functions, source_lines) + + assert result is not None + assert result["expansion_start"] == 10 + assert result["expansion_end"] == 30 + + def test_no_expansion_for_change_outside_function(self): + hunk = { + "header": "@@ -1,3 +1,4 @@", + "old_start": 1, + "old_count": 3, + "new_start": 1, + "new_count": 4, + "lines": ["+import new_module", " import os"], + "changed_new_lines": [1], + } + # Function starts at line 10 + functions = [FunctionInfo( + name="test_func", + file="test.py", + line_start=10, + line_end=30, + parameters=[], + )] + source_lines = [f"line {i}" for i in range(1, 35)] + + result = _expand_hunk_to_functions(hunk, functions, source_lines) + + # Should return hunk unchanged (no enclosing function) + assert result == hunk + + def test_uses_innermost_function_for_nested(self): + hunk = { + "header": "@@ -15,3 +15,3 @@", + "old_start": 15, + "old_count": 3, + "new_start": 15, + "new_count": 3, + "lines": ["- old", "+ new", " pass"], + "changed_new_lines": [15], + } + # Outer function: lines 5-25, inner function: lines 12-18 + functions = [ + FunctionInfo(name="outer", file="test.py", line_start=5, line_end=25, parameters=[]), + FunctionInfo(name="inner", file="test.py", line_start=12, line_end=18, parameters=[]), + ] + source_lines = [f"line {i}" for i in range(1, 30)] + + result = _expand_hunk_to_functions(hunk, functions, source_lines) + + # Should use inner function boundaries + assert result["expansion_start"] == 12 + assert result["expansion_end"] == 18 + + def test_expands_multiple_functions_in_one_hunk(self): + hunk = { + "header": "@@ -20,10 +20,12 @@", + "old_start": 20, + "old_count": 10, + "new_start": 20, + "new_count": 12, + "lines": ["-old", "+new"], + "changed_new_lines": [22, 35], + } + functions = [ + FunctionInfo(name="func1", file="test.py", line_start=15, line_end=25, parameters=[]), + FunctionInfo(name="func2", file="test.py", line_start=30, line_end=40, parameters=[]), + ] + source_lines = [f"line {i}" for i in range(1, 50)] + + result = _expand_hunk_to_functions(hunk, functions, source_lines) + + # Should expand to cover both functions + assert result["expansion_start"] == 15 + assert result["expansion_end"] == 40 + + +class TestMergeHunks: + """Tests for _merge_hunks function.""" + + def test_returns_single_hunk_unchanged(self): + hunks = [{"expansion_start": 10, "expansion_end": 30}] + merged = _merge_hunks(hunks) + assert merged == hunks + + def test_merges_overlapping_hunks(self): + hunks = [ + {"expansion_start": 10, "expansion_end": 30, "original_hunk": {"header": "@@ -17,7 +17,7 @@"}}, + {"expansion_start": 25, "expansion_end": 50, "original_hunk": {"header": "@@ -40,5 +40,5 @@"}}, + ] + merged = _merge_hunks(hunks) + + assert len(merged) == 1 + assert merged[0]["expansion_start"] == 10 + assert merged[0]["expansion_end"] == 50 + + def test_merges_adjacent_hunks(self): + hunks = [ + {"expansion_start": 10, "expansion_end": 20}, + {"expansion_start": 21, "expansion_end": 30}, # Adjacent (gap = 1) + ] + merged = _merge_hunks(hunks) + + assert len(merged) == 1 + assert merged[0]["expansion_start"] == 10 + assert merged[0]["expansion_end"] == 30 + + def test_keeps_separate_non_overlapping_hunks(self): + hunks = [ + {"expansion_start": 10, "expansion_end": 20}, + {"expansion_start": 30, "expansion_end": 40}, # Gap of 9 lines + ] + merged = _merge_hunks(hunks) + + assert len(merged) == 2 + + def test_sorts_hunks_by_start(self): + hunks = [ + {"expansion_start": 50, "expansion_end": 60}, + {"expansion_start": 10, "expansion_end": 20}, + ] + merged = _merge_hunks(hunks) + + assert merged[0]["expansion_start"] == 10 + assert merged[1]["expansion_start"] == 50 + + +class TestRebuildPatch: + """Tests for _rebuild_patch function.""" + + def test_rebuilds_single_expanded_hunk(self): + raw_patch = """@@ -17,7 +17,7 @@ def generate_token(user_id: str, ttl_hours: int = 24) -> str: + expiry = datetime.utcnow() + timedelta(hours=ttl_hours) + payload = f"{user_id}:{expiry.isoformat()}" + signature = hashlib.sha256(f"{payload}{SECRET_KEY}".encode()).hexdigest() +- return f"{payload}:{signature}" ++ return f"{payload}.{signature}" + + + def verify_token(token: str) -> bool:""" + + source_lines = [f"line {i}" for i in range(1, 50)] + merged_hunks = [ + { + "original_hunk": { + "header": "@@ -17,7 +17,7 @@", + "lines": [ + " expiry = datetime.utcnow() + timedelta(hours=ttl_hours)", + ' payload = f"{user_id}:{expiry.isoformat()}"', + ' signature = hashlib.sha256(f"{payload}{SECRET_KEY}".encode()).hexdigest()', + '- return f"{payload}:{signature}"', + '+ return f"{payload}.{signature}"', + "", + "", + " def verify_token(token: str) -> bool:", + ], + }, + "expansion_start": 10, + "expansion_end": 30, + "hunk_start_new": 17, + "hunk_end_new": 24, + } + ] + + result = _rebuild_patch(raw_patch, merged_hunks, source_lines) + + assert result is not None + assert "@@ -10," in result # New header starts at expansion_start + + def test_keeps_header_lines(self): + raw_patch = """diff --git a/src/main.py b/src/main.py +index 123..456 789 +--- a/src/main.py ++++ b/src/main.py +@@ -1,3 +1,3 @@ +-old ++new""" + + source_lines = ["line1", "line2", "line3"] + merged_hunks = [ + { + "original_hunk": { + "header": "@@ -1,3 +1,3 @@", + "lines": ["-old", "+new"], + }, + "expansion_start": 1, + "expansion_end": 3, + "hunk_start_new": 1, + "hunk_end_new": 2, + } + ] + + result = _rebuild_patch(raw_patch, merged_hunks, source_lines) + + assert "diff --git" in result + assert "index 123..456" in result + + def test_returns_none_for_empty_hunks(self): + result = _rebuild_patch("patch", [], []) + assert result is None + + +class TestCompactPatch: + """Tests for compact_patch function.""" + + def test_returns_original_for_empty_patch(self): + result = compact_patch("", Path("test.py"), Path("/repo")) + assert result == "" + + def test_returns_original_when_no_functions_found(self, tmp_path: Path): + # Create a Python file without functions + source_file = tmp_path / "test.py" + source_file.write_text("x = 1\ny = 2\n") + + patch = """@@ -1,2 +1,2 @@ +-x = 1 ++x = 2 + y = 2""" + + mock_parser = MagicMock() + mock_parser.available = True + mock_parser.find_function_definitions.return_value = [] + + result = compact_patch(patch, Path("test.py"), tmp_path, mock_parser) + assert result == patch + + def test_returns_original_when_treesitter_unavailable(self, tmp_path: Path): + patch = """@@ -1,3 +1,3 @@ +-old ++new""" + + mock_parser = MagicMock() + mock_parser.available = False + + result = compact_patch(patch, Path("test.py"), tmp_path, mock_parser) + assert result == patch + + def test_returns_original_for_non_code_file(self, tmp_path: Path): + patch = """@@ -1,3 +1,3 @@ +-old ++new""" + + result = compact_patch(patch, Path("README.md"), tmp_path) + assert result == patch + + +class TestCompactPatches: + """Tests for compact_patches function.""" + + def test_processes_multiple_scopes(self, tmp_path: Path, monkeypatch): + # Create source files + (tmp_path / "src").mkdir() + (tmp_path / "src" / "main.py").write_text("x = 1\n") + (tmp_path / "src" / "utils.py").write_text("y = 2\n") + + scope1 = ScopeResult( + repo="owner/repo", + subroot="pkg1", + scope_type=ScopeType.LIBRARY, + reason="test", + changed_files=[ + ChangedFile( + filename="src/main.py", + status=FileStatus.MODIFIED, + patch="@@ -1,3 +1,3 @@\n-old\n+new", + ) + ], + ) + scope2 = ScopeResult( + repo="owner/repo", + subroot="pkg2", + scope_type=ScopeType.SERVICE, + reason="test", + changed_files=[ + ChangedFile( + filename="src/utils.py", + status=FileStatus.MODIFIED, + patch="@@ -5,3 +5,3 @@\n-old\n+new", + ) + ], + ) + scopes = [scope1, scope2] + + # Mock TreeSitterParser to return empty functions + call_count = 0 + def mock_find_functions(self, file_path): + nonlocal call_count + call_count += 1 + return [] + + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", + mock_find_functions, + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.available", True + ) + + compact_patches(scopes, tmp_path) + + # Should have tried to find functions in both files + assert call_count == 2 + + def test_skips_deleted_files(self, tmp_path: Path, monkeypatch): + scope = ScopeResult( + repo="owner/repo", + subroot=".", + scope_type=ScopeType.LIBRARY, + reason="test", + changed_files=[ + ChangedFile( + filename="src/deleted.py", + status=FileStatus.REMOVED, + patch="@@ -1,3 +0,0 @@\n-line1\n-line2\n-line3", + ) + ], + ) + + call_count = 0 + def mock_find_functions(self, file_path): + nonlocal call_count + call_count += 1 + return [] + + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", + mock_find_functions, + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.available", True + ) + + compact_patches([scope], tmp_path) + + # Should not try to find functions in deleted files + assert call_count == 0 + + def test_skips_files_without_patches(self, tmp_path: Path, monkeypatch): + scope = ScopeResult( + repo="owner/repo", + subroot=".", + scope_type=ScopeType.LIBRARY, + reason="test", + changed_files=[ + ChangedFile( + filename="src/empty.py", + status=FileStatus.MODIFIED, + patch=None, + ) + ], + ) + + call_count = 0 + def mock_find_functions(self, file_path): + nonlocal call_count + call_count += 1 + return [] + + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", + mock_find_functions, + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.available", True + ) + + compact_patches([scope], tmp_path) + + assert call_count == 0 + + def test_handles_compaction_failure_gracefully(self, tmp_path: Path, monkeypatch): + # Create source file + source_file = tmp_path / "test.py" + source_file.write_text("def func():\n pass\n") + + scope = ScopeResult( + repo="owner/repo", + subroot=".", + scope_type=ScopeType.LIBRARY, + reason="test", + changed_files=[ + ChangedFile( + filename="test.py", + status=FileStatus.MODIFIED, + patch="@@ -1,3 +1,3 @@\n-old\n+new", + ) + ], + ) + + def mock_find_functions(self, file_path): + raise Exception("parse error") + + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", + mock_find_functions, + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None + ) + monkeypatch.setattr( + "codespy.tools.git.patch_utils.TreeSitterParser.available", True + ) + + # Should not raise + compact_patches([scope], tmp_path) + + # Original patch should be preserved + assert scope.changed_files[0].patch == "@@ -1,3 +1,3 @@\n-old\n+new" From abd8f11511a96ffaaf2109c314b286c3073528b8 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 9 Aug 2026 19:20:08 +0200 Subject: [PATCH 36/79] wip --- .../agents/reviewer/modules/doc_reviewer.py | 7 ++---- .../agents/reviewer/modules/helpers.py | 22 ++++++++++++++++++- .../agents/reviewer/modules/summarizer.py | 11 ++++++++-- src/codespy/agents/reviewer/reviewer.py | 3 +++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index d6bc13f..5dad453 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -14,6 +14,7 @@ from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, + build_patches, issues_to_markdown, make_scope_relative, resolve_scope_root, @@ -107,11 +108,7 @@ def __init__(self) -> None: def _build_patches(self, scope: ScopeResult) -> str: """Build patches representation for review.""" - parts: list[str] = [] - for f in scope.changed_files: - if f.patch: - parts.append(f"--- {f.filename} ---\n{f.patch}") - return "\n\n".join(parts) + return build_patches(scope.changed_files) async def aforward( self, diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index ec22eae..9220c7e 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -5,7 +5,7 @@ import logging import os from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence from codespy.tools.git.models import ChangedFile from codespy.agents.reviewer.models import Issue @@ -182,6 +182,26 @@ def restore_repo_paths(issues: list[Issue], subroot: str) -> None: issue.filename = prefix + issue.filename +def build_patches(files: Sequence[ChangedFile]) -> str: + """Build a patches string from a sequence of changed files. + + Each patch is prefixed with its filename in the format: + --- {filename} --- + {patch content} + + Args: + files: Sequence of ChangedFile objects + + Returns: + Concatenated patches string, or empty string if no patches + """ + parts: list[str] = [] + for f in files: + if f.patch: + parts.append(f"--- {f.filename} ---\n{f.patch}") + return "\n\n".join(parts) + + def issues_to_markdown(issues: list[Issue]) -> str: """Format a list of issues as a compact Markdown report. diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index db8aa45..11de9b4 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -16,8 +16,8 @@ class PRSummarySignature(dspy.Signature): """Summarize what a merge request does in 2-3 sentences. You are a busy Principal Engineer. Be extremely terse. State facts only. - Based on the title, description, and changed file paths, describe - what this MR accomplishes. No polite filler. No conversational language. + Based on the title, description, changed file paths, and code patches, + describe what this MR accomplishes. No polite filler. No conversational language. """ mr_title: str = dspy.InputField(desc="Title of the merge request") @@ -25,6 +25,9 @@ class PRSummarySignature(dspy.Signature): changed_file_paths: list[str] = dspy.InputField( desc="List of changed file paths from the MR" ) + patches: str = dspy.InputField( + desc="Unified diff patches showing code changes. Each patch is prefixed with the filename." + ) summary: str = dspy.OutputField( desc="2-3 sentence summary of what this MR accomplishes" @@ -45,6 +48,7 @@ def forward( mr_description: str, mr_number: int, changed_file_paths: list[str], + patches: str, repo_slug: str, run_id: str | None = None, ) -> tuple[str, ContextMap | None]: @@ -55,6 +59,7 @@ def forward( mr_description: Description/body of the MR mr_number: MR/PR number changed_file_paths: List of changed file paths + patches: Unified diff patches showing code changes repo_slug: Host-qualified repo slug for episode path run_id: Pipeline run identifier @@ -86,6 +91,7 @@ def forward( mr_title=mr_title, mr_description=mr_description, changed_file_paths=changed_file_paths, + patches=patches, ) mem.end_episode( get_memory_store(self._settings), @@ -97,6 +103,7 @@ def forward( mr_title=mr_title, mr_description=mr_description, changed_file_paths=changed_file_paths, + patches=patches, ) logger.info(f"PR summary: {result.summary[:80]}...") diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index ad9e5e7..2af84bc 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -32,6 +32,7 @@ Summarizer, SupplyChainAuditor, ) +from codespy.agents.reviewer.modules.helpers import build_patches logger = logging.getLogger(__name__) @@ -179,11 +180,13 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Step 1: Run Summarizer (before scope identification) changed_file_paths = [f.filename for f in mr.changed_files] + patches = build_patches(mr.changed_files) pr_summary, summarizer_memory = self.summarizer( mr_title=mr.title, mr_description=mr.body or "No description provided.", mr_number=mr.number, changed_file_paths=changed_file_paths, + patches=patches, repo_slug=mr.repo_slug, run_id=run_id, ) From e7daacc2ab7fe7c72ca6d47826210f6c550f57ba Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 9 Aug 2026 19:26:28 +0200 Subject: [PATCH 37/79] wip --- src/codespy/tools/git/patch_utils.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index afa0e16..99ab90f 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -410,16 +410,11 @@ def _rebuild_patch( if line_num <= len(source_lines): new_hunk_lines.append(f" {source_lines[line_num - 1]}") - # Add original hunk lines (excluding context lines that are now part of expansion) + # Add all original hunk lines (context + changes). + # No overlap with expansion context: pre-expansion ends at hunk_start_new, + # post-expansion begins at hunk_end_new + 1, so interior context is unique. original_lines = original_hunk.get("lines", []) - for line in original_lines: - if line.startswith(" "): - # Check if this context line is within our expansion range - # We already added the expansion context, so skip original context - # that overlaps with expansion - pass - else: - new_hunk_lines.append(line) + new_hunk_lines.extend(original_lines) # Add context lines after the original hunk (from hunk_end_new + 1 to expansion_end) for line_num in range(hunk_end_new + 1, expansion_end + 1): From 592090b0cb9a7c8576d20f595051a6645e08da63 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 9 Aug 2026 21:54:54 +0200 Subject: [PATCH 38/79] wip --- .env.example | 22 +++++++++---------- README.md | 13 +++++++---- .../agents/reviewer/modules/code_reviewer.py | 2 +- .../agents/reviewer/modules/doc_reviewer.py | 4 ++-- .../reviewer/modules/scope_identifier.py | 2 +- .../reviewer/modules/supply_chain_auditor.py | 2 +- src/codespy/agents/reviewer/reviewer.py | 2 +- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index eb74af9..78fa7f6 100644 --- a/.env.example +++ b/.env.example @@ -100,7 +100,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # but not deep reasoning. Recommended: anthropic/claude-sonnet-4-5-20250929 # or equivalent. # -# Cheap (SUMMARIZATION_MODEL): PR summary generation. Simple synthesis. +# Cheap (SUMMARY_MODEL): PR summary generation. Simple synthesis. # Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # # Cheap (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory @@ -110,7 +110,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # # By default, all models fall back to DEFAULT_MODEL. To optimize costs: # EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 -# SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 +# SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 @@ -248,7 +248,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - DOC (compares patches against documentation) # - SCOPE (code scope detection) # - SUPPLY_CHAIN (supply chain security analysis) -# - SUMMARIZATION (PR summary generation) +# - SUMMARY (PR summary generation) # # Available settings per signature: # - ENABLED (true/false) @@ -311,12 +311,12 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Unused: scope uses question_field="mr_title", so no inputs are serialized. # SCOPE_MEMORY_MAX_QUESTION_TOKENS= -# SUMMARIZATION_ENABLED=true -# SUMMARIZATION_MODEL=anthropic/claude-haiku-4-5-20251001 -# SUMMARIZATION_MEMORY_ENABLED=true -# SUMMARIZATION_MEMORY_MAX_REFLECTS=1 -# SUMMARIZATION_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 -# SUMMARIZATION_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 -# SUMMARIZATION_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# SUMMARIZATION_MEMORY_MAX_QUESTION_TOKENS=2048 +# SUMMARY_ENABLED=true +# SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 +# SUMMARY_MEMORY_ENABLED=true +# SUMMARY_MEMORY_MAX_REFLECTS=1 +# SUMMARY_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS=8192 +# SUMMARY_MEMORY_MAX_QUESTION_TOKENS=2048 diff --git a/README.md b/README.md index 1b2d56c..54718db 100644 --- a/README.md +++ b/README.md @@ -693,16 +693,21 @@ Tree-sitter based parsing for context-aware analysis: | Language | Extensions | Features | |----------|-----------|----------| -| Python | `.py` | Functions, classes, imports | -| JavaScript | `.js`, `.jsx` | Functions, classes, imports | -| TypeScript | `.ts`, `.tsx` | Functions, classes, interfaces | +| Bash | `.sh`, `.bash` | Functions, commands | +| C/C++ | `.c`, `.cpp`, `.h`, `.hpp` | Functions, classes, structs | +| C# | `.cs` | Methods, classes, interfaces | | Go | `.go` | Functions, structs, interfaces | | Java | `.java` | Methods, classes, packages | +| JavaScript | `.js`, `.jsx` | Functions, classes, imports | | Kotlin | `.kt` | Functions, classes, objects | -| Swift | `.swift` | Functions, classes, structs | | Objective-C | `.m`, `.h` | Methods, interfaces, protocols | +| PHP | `.php` | Functions, classes, namespaces | +| Python | `.py` | Functions, classes, imports | +| Ruby | `.rb` | Methods, classes, modules | | Rust | `.rs` | Functions, structs, traits, impl blocks | +| Swift | `.swift` | Functions, classes, structs | | Terraform | `.tf` | Resources, data sources, modules, variables | +| TypeScript | `.ts`, `.tsx` | Functions, classes, interfaces | All languages are supported for security, bug, and documentation analysis. diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index ae3197d..4d46206 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -285,7 +285,7 @@ async def aforward( f" Scope {scope.subroot}: {len(issues)} code review issues" ) except Exception as e: - logger.error(f"Code review failed for scope {scope.subroot}: {e}") + logger.error(f"Code review failed for scope {scope.subroot}: {e}", exc_info=True) finally: await cleanup_mcp_contexts(contexts) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 5dad453..efd9a66 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -150,7 +150,7 @@ async def aforward( try: documentation = extract_documentation(scope_root) except Exception as e: - logger.error(f"Doc extraction failed for scope {scope.subroot}: {e}") + logger.error(f"Doc extraction failed for scope {scope.subroot}: {e}", exc_info=True) documentation = "" if not documentation.strip(): logger.debug( @@ -219,7 +219,7 @@ async def aforward( f" Scope {scope.subroot}: {len(issues)} doc issues" ) except Exception as e: - logger.error(f"Doc review failed for scope {scope.subroot}: {e}") + logger.error(f"Doc review failed for scope {scope.subroot}: {e}", exc_info=True) logger.info(f"Doc review found {len(all_issues)} issues") # Merge all scope context maps into one module-level map diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py index 6bcfde8..6579b9b 100644 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ b/src/codespy/agents/reviewer/modules/scope_identifier.py @@ -364,7 +364,7 @@ async def aforward( # Capture final memory after successful execution final_memory = mem.cmap.model_copy(deep=True) if mem else (review_context.memory if review_context else None) except Exception as e: - logger.error(f"Agent failed: {e}") + logger.error(f"Agent failed: {e}", exc_info=True) scopes = [ScopeResult( repo=repo, subroot=".", diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 37a0cf6..fd7ba22 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -376,7 +376,7 @@ async def aforward( all_issues.extend(issues) logger.debug(f" Supply chain security in scope {scope.subroot}: {len(issues)} issues") except Exception as e: - logger.error(f"Error analyzing supply chain in scope {scope.subroot}: {e}") + logger.error(f"Error analyzing supply chain in scope {scope.subroot}: {e}", exc_info=True) finally: await cleanup_mcp_contexts(scoped_contexts) finally: diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 2af84bc..b21ac90 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -122,7 +122,7 @@ async def _run_review_modules( context_maps: dict[str, ContextMap | None] = {} for i, result in enumerate(results): if isinstance(result, Exception): - logger.error(f"{module_names[i]} failed: {result}") + logger.error(f"{module_names[i]} failed: {result}", exc_info=result) elif result is not None: issues, ctx_map = result all_issues.extend(issues) From 32a92d37f9df0c10c59eed158cdad4a7b3e8364a Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 9 Aug 2026 23:51:22 +0200 Subject: [PATCH 39/79] wip --- .../agents/reviewer/modules/__init__.py | 4 +- .../reviewer/modules/scope_identifier.py | 435 --------- .../agents/reviewer/modules/scope_resolver.py | 839 ++++++++++++++++++ src/codespy/agents/reviewer/reviewer.py | 8 +- tests/test_scope_resolver.py | 212 +++++ 5 files changed, 1057 insertions(+), 441 deletions(-) delete mode 100644 src/codespy/agents/reviewer/modules/scope_identifier.py create mode 100644 src/codespy/agents/reviewer/modules/scope_resolver.py create mode 100644 tests/test_scope_resolver.py diff --git a/src/codespy/agents/reviewer/modules/__init__.py b/src/codespy/agents/reviewer/modules/__init__.py index 549d770..b0ebcaa 100644 --- a/src/codespy/agents/reviewer/modules/__init__.py +++ b/src/codespy/agents/reviewer/modules/__init__.py @@ -3,7 +3,7 @@ from codespy.agents.reviewer.modules.auditor import Auditor from codespy.agents.reviewer.modules.code_reviewer import CodeReviewer from codespy.agents.reviewer.modules.doc_reviewer import DocReviewer -from codespy.agents.reviewer.modules.scope_identifier import ScopeIdentifier +from codespy.agents.reviewer.modules.scope_resolver import ScopeResolver from codespy.agents.reviewer.modules.summarizer import Summarizer from codespy.agents.reviewer.modules.supply_chain_auditor import SupplyChainAuditor @@ -11,7 +11,7 @@ "Auditor", "CodeReviewer", "DocReviewer", - "ScopeIdentifier", + "ScopeResolver", "Summarizer", "SupplyChainAuditor", ] diff --git a/src/codespy/agents/reviewer/modules/scope_identifier.py b/src/codespy/agents/reviewer/modules/scope_identifier.py deleted file mode 100644 index 6579b9b..0000000 --- a/src/codespy/agents/reviewer/modules/scope_identifier.py +++ /dev/null @@ -1,435 +0,0 @@ -"""Scope identifier module for detecting code scopes in repositories.""" - -import asyncio -import logging -from pathlib import Path -from typing import Any - -import dspy # type: ignore[import-untyped] -from pydantic import BaseModel, Field - -from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.memory.hippocampus import ContextMap -from codespy.agents.reviewer.models import PackageManifest, ReviewContext, ScopeResult, ScopeType -from codespy.config import get_settings -from codespy.config_memory import get_memory_store -from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file -from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server - -logger = logging.getLogger(__name__) - - -class ScopeAssignment(BaseModel): - """LLM-friendly scope assignment with string file paths. - - This intermediate model is used for LLM output since the LLM can only - produce string file paths, not full ChangedFile objects with patches/content. - It gets converted to ScopeResult with proper ChangedFile objects after LLM call. - """ - - subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") - scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") - has_changes: bool = Field( - default=False, description="Whether this scope has changed files from PR" - ) - is_dependency: bool = Field( - default=False, description="Whether this scope depends on a changed scope" - ) - confidence: float = Field( - default=0.8, ge=0.0, le=1.0, description="Confidence score for scope identification" - ) - language: str | None = Field(default=None, description="Primary language detected") - package_manifest: PackageManifest | None = Field( - default=None, description="Package manifest info if present" - ) - changed_files: list[str] = Field( - default_factory=list, description="Changed file paths belonging to this scope" - ) - reason: str = Field(description="Explanation for why this scope was identified") - - -def scope_assignments_to_markdown(assignments: list[ScopeAssignment]) -> str: - """Format scope assignments as a compact Markdown report. - - Intended as an ``Episode`` artifact (see ``Hippocampus.aend_episode``) so - the repo-level episode carries a human-readable snapshot of the scopes - identified for this call, alongside the consolidated context map. - - Args: - assignments: Scope assignments produced by the agent for this call. - - Returns: - Markdown text. If ``assignments`` is empty, a short "no scopes" note. - """ - if not assignments: - return "No scopes identified." - - lines = [f"## Scopes ({len(assignments)})", ""] - for assignment in assignments: - lines.extend([ - f"### {assignment.subroot}", - "", - f"**Type:** {assignment.scope_type.value}", - f"**Confidence:** {assignment.confidence}", - f"**Has changes:** {assignment.has_changes}", - f"**Is dependency:** {assignment.is_dependency}", - f"**Files:** {len(assignment.changed_files)}", - "", - assignment.reason, - "", - "---", - "", - ]) - return "\n".join(lines) - - -class ScopeIdentifierSignature(dspy.Signature): - """Identify code scopes in a repository for a merge request. - - You have tools to clone the repository, explore its filesystem, and analyze code. - Your goal is to identify logical code boundaries (scopes) and assign each changed file to exactly one scope. - - STEP 1 - ANALYZE CHANGED FILE PATHS (before cloning): - The changed file paths are your MOST IMPORTANT signal for scope detection. - 1. Extract common directory prefixes from changed files to find candidate scopes - 2. Look for scope indicator patterns at ANY DEPTH in the path: - - svc/, services/, microservices/ → likely a service scope - - libs/, packages/, shared/, common/, core/ → likely a library scope - - apps/, web/, frontend/, mobile/ → likely an application scope - - scripts/, bin/, tools/, hack/, ci/, .github/, .gitlab/ → likely a script scope - 3. EXAMPLES of nested scope detection from file paths: - - Files: mono/svc/my-service-v1/internal/handler.go, mono/svc/my-service-v1/cmd/main.go - → Candidate scope: mono/svc/my-service-v1 (the "svc/" pattern indicates service) - - Files: platform/packages/auth/src/index.ts, platform/packages/auth/package.json - → Candidate scope: platform/packages/auth (the "packages/" pattern indicates library) - - Files: company/backend/services/user-api/main.go - → Candidate scope: company/backend/services/user-api - 4. Group files by their longest common directory prefix that contains a scope indicator - - STEP 2 - ACCESS THE REPOSITORY: - - If is_local is True: - The repository is ALREADY available at target_repo_path. Do NOT clone. - Skip directly to STEP 3 and use filesystem tools to explore the repo. - - If is_local is False: - Clone using clone_repository tool: - 1. Use the repo_owner, repo_name, and head_sha provided in the inputs - 2. Clone to the target_repo_path provided - 3. Derive sparse_paths from candidate scopes identified in STEP 1: - - Include each candidate scope directory - - Example: ["mono/svc/my-service-v1/", "libs/common/"] - 4. Use depth=1 for fastest clone (single commit) - - STEP 3 - VERIFY SCOPES WITH PACKAGE MANIFESTS: - For each candidate scope from STEP 1: - 1. Check if a package manifest exists at that path (go.mod, package.json, pyproject.toml, Cargo.toml, etc.) - 2. If found → CONFIRM that directory as the scope root - 3. If NOT found → Walk UP parent directories until you find a package manifest - - Example: If candidate is mono/svc/my-service-v1/internal, check: - * mono/svc/my-service-v1/internal/go.mod (not found) - * mono/svc/my-service-v1/go.mod (FOUND → this is the scope) - 4. The directory containing the package manifest is the authoritative scope root - - SCOPE TYPE CLASSIFICATION: - These patterns can appear at ANY NESTING DEPTH - not just at the repository root! - - library: Shared code that others import - * Patterns at any depth: */libs/*, */packages/*, */shared/*, */common/*, */core/* - * Multiple consumers importing from this scope - * Generic/reusable code patterns - - service: Isolated microservice with APIs - * Patterns at any depth: */services/*, */microservices/*, */svc/* - * Own package manifest, often with server/API code - * HTTP handlers, gRPC definitions, message consumers - - application: Standalone app or frontend - * Patterns at any depth: */apps/*, */web/*, */frontend/*, */mobile/* - * Entry points (main.go, index.ts, App.tsx) - * UI components, routing, state management - - script: Build/deployment scripts, tooling - * Patterns at any depth: */scripts/*, */bin/*, */tools/*, */hack/*, */ci/*, */.github/* - * Shell scripts, Makefiles, Dockerfiles - * CI/CD configuration, deployment scripts - - MONO-REPO PATTERNS (can be nested!): - - Check root AND nested package.json for workspaces configuration - - Look for lerna.json, pnpm-workspace.yaml, turbo.json at various depths - - Scope indicator directories (packages/, services/, apps/, svc/) can appear at any level: - * repo/services/api/ ← traditional mono-repo - * repo/mono/svc/user-api/ ← nested mono-repo - * repo/platform/backend/services/api/ ← deeply nested - - PACKAGE MANIFESTS TO DETECT: - - package.json (npm) with lock files: package-lock.json, yarn.lock, pnpm-lock.yaml - - pyproject.toml (pip) with lock files: poetry.lock, uv.lock - - go.mod (go) with lock file: go.sum - - Cargo.toml (cargo) with lock file: Cargo.lock - - pom.xml (maven), build.gradle (gradle), composer.json (composer), Gemfile (bundler) - - CRITICAL RULES: - 1. EVERY changed file must be assigned to exactly ONE scope - 2. Don't create overlapping scopes (parent contains child) - 3. ALWAYS prefer the most specific scope - the deepest directory with a package manifest - - If files are in mono/svc/my-service-v1/, use that as scope, NOT "." or "mono/" - 4. Use "." as scope ONLY when: - - Files are truly at the repo root with no nested project structure - - No package manifest exists at any deeper level - - Changed files span multiple unrelated directories with no common scope indicator - 5. Use tools to verify package manifest existence - don't guess - 6. Trust the file paths - if they contain svc/, services/, packages/ etc., that's a strong scope signal - - OUTPUT EFFICIENCY: Group files by common directory prefix in reasoning. Do not reason about each file path individually. - Keep each reasoning step to 1-2 sentences. - """ - - changed_files: list[str] = dspy.InputField( - desc="List of changed file paths from the MR. Use these to derive sparse_paths for efficient cloning." - ) - repo_owner: str = dspy.InputField(desc="Repository owner/namespace (e.g., 'facebook' or 'group/subgroup'). Used for cloning (remote only).") - repo_name: str = dspy.InputField(desc="Repository name (e.g., 'react'). Used for cloning (remote only).") - head_sha: str = dspy.InputField(desc="Git commit SHA to checkout. Used for cloning (remote only).") - target_repo_path: str = dspy.InputField( - desc="Absolute path to the repository. For remote reviews, clone here. For local reviews, the repo is already here." - ) - mr_title: str = dspy.InputField(desc="MR title for additional context") - mr_description: str = dspy.InputField(desc="MR description for additional context") - is_local: bool = dspy.InputField( - desc="Whether the repository is already available locally at target_repo_path. If True, skip cloning and go directly to filesystem exploration." - ) - - scopes: list[ScopeAssignment] = dspy.OutputField( - desc="Identified scopes. Every changed file must appear in exactly one scope. Use concise reasons (<2 sentences)." - ) - - -class ScopeIdentifier(dspy.Module): - """Agentic scope identifier using ReAct pattern with MCP tools. - - This module uses an LLM agent to explore the repository structure - and identify logical code scopes for focused code review. - """ - - def __init__(self) -> None: - """Initialize the scope identifier.""" - super().__init__() - self._cost_tracker = get_cost_tracker() - self._settings = get_settings() - - async def _create_mcp_tools(self, repo_path: Path, is_local: bool = False) -> tuple[list[Any], list[Any]]: - """Create DSPy tools from MCP servers. - - Args: - repo_path: Path to the repository root - is_local: If True, skip git server (repo already on disk, no cloning needed) - """ - tools: list[Any] = [] - contexts: list[Any] = [] - tools_dir = Path(__file__).parent.parent.parent.parent / "tools" - repo_path_str = str(repo_path) - caller = "scope_identifier" - tools.extend(await connect_mcp_server(tools_dir / "storage" / "filesystem" / "server.py", [repo_path_str], contexts, caller)) - tools.extend(await connect_mcp_server(tools_dir / "parsers" / "ripgrep" / "server.py", [repo_path_str], contexts, caller)) - tools.extend(await connect_mcp_server(tools_dir / "parsers" / "treesitter" / "server.py", [repo_path_str], contexts, caller)) - if not is_local: - tools.extend(await connect_mcp_server(tools_dir / "git" / "server.py", [], contexts, caller)) - return tools, contexts - - async def aforward( - self, - mr: MergeRequest, - repo_path: Path, - is_local: bool = False, - run_id: str | None = None, - review_context: ReviewContext | None = None, - ) -> tuple[list[ScopeResult], ContextMap | None]: - """Identify scopes in the repository for the given MR. - - Args: - mr: The merge request to analyze - repo_path: Path to the repository root - is_local: If True, repo is already on disk (skip cloning) - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run (see ``Hippocampus.run_id``) - review_context: ReviewContext containing PR identity and inherited memory - - Returns: - Tuple of (list of ScopeResult, final context map or None) - """ - # Get excluded directories from settings - excluded_dirs = self._settings.excluded_directories - - # Filter out binary, lock files, minified files, excluded directories, etc. - reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] - excluded_count = len(mr.changed_files) - len(reviewable_files) - if excluded_count > 0: - excluded_files = [f.filename for f in mr.changed_files if not should_review_file(f, excluded_dirs)] - logger.info(f"Excluded {excluded_count} non-reviewable files: {excluded_files[:10]}{'...' if len(excluded_files) > 10 else ''}") - - if not reviewable_files: - logger.warning("No reviewable files in MR - all files are binary, lock files, or in excluded directories") - return [], review_context.memory if review_context else None - - # Repo identifier for this review, stamped onto every ScopeResult - # (used by Hippocampus memory to build the episode path). Uses the - # host-qualified slug (e.g. "github.com/owner/repo") so local and - # remote reviews of the same repository share the same memory path. - repo = mr.repo_slug - - # Check if signature is enabled - if not self._settings.is_signature_enabled("scope"): - logger.warning("scope is disabled - using fallback single scope") - return [ScopeResult( - repo=repo, - subroot=".", - scope_type=ScopeType.APPLICATION, - has_changes=True, - is_dependency=False, - confidence=0.5, - language=None, - package_manifest=None, - changed_files=reviewable_files, - reason="Scope identification disabled - fallback to single scope", - )], review_context.memory if review_context else None - - tools, contexts = await self._create_mcp_tools(repo_path, is_local=is_local) - changed_file_paths = [f.filename for f in reviewable_files] - # Build map from filename to ChangedFile for post-processing - changed_files_map: dict[str, ChangedFile] = {f.filename: f for f in reviewable_files} - try: - # Get per-signature config. The model, temperature, and reasoning - # effort are applied by SignatureContext("scope") below. - max_iters = self._settings.get_max_iters("scope") - - # Create ReAct agent - - agent = dspy.ReAct( - signature=ScopeIdentifierSignature, - tools=tools, - max_iters=max_iters, - ) - logger.info(f"Identifying scopes for {len(changed_file_paths)} changed files...") - mem: Hippocampus | None = None - final_memory: ContextMap | None = review_context.memory if review_context else None - async with SignatureContext("scope", self._cost_tracker): - if self._settings.get_memory_enabled("scope"): - question = ( - f"identify scopes of {review_context.pr_context.repo_slug}: " - f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" - ) if review_context else None - mem = Hippocampus( - agent, - budget=self._settings.get_memory_budget("scope"), - max_reflects=self._settings.get_memory_max_reflects("scope"), - question=question, - task_name="scope", - run_id=run_id, - initial_memory=review_context.memory if review_context else None, - ) - result = await mem.aforward( - changed_files=changed_file_paths, - repo_owner=mr.repo_owner, - repo_name=mr.repo_name, - head_sha=mr.head_sha, - target_repo_path=str(repo_path), - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", - is_local=is_local, - ) - # Repo-level episode: subroot "." (no scope object exists yet). - dir_path = f"/{repo}/" - await mem.aend_episode( - get_memory_store(self._settings), - dir_path, - artifacts={ - "scopes": scope_assignments_to_markdown(result.scopes) - }, - ) - else: - result = await agent.acall( - changed_files=changed_file_paths, - repo_owner=mr.repo_owner, - repo_name=mr.repo_name, - head_sha=mr.head_sha, - target_repo_path=str(repo_path), - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", - is_local=is_local, - ) - scope_assignments: list[ScopeAssignment] = result.scopes - # Ensure we got valid scopes - if not scope_assignments: - raise ValueError("No scopes returned by agent") - # Convert ScopeAssignment (with string paths) to ScopeResult (with ChangedFile objects) - scopes = self._convert_assignments_to_results( - scope_assignments, changed_files_map, repo - ) - # Capture final memory after successful execution - final_memory = mem.cmap.model_copy(deep=True) if mem else (review_context.memory if review_context else None) - except Exception as e: - logger.error(f"Agent failed: {e}", exc_info=True) - scopes = [ScopeResult( - repo=repo, - subroot=".", - scope_type=ScopeType.APPLICATION, - has_changes=True, - is_dependency=False, - confidence=0.5, - language=None, - package_manifest=None, - changed_files=list(mr.changed_files), - reason=f"Fallback due to agent error: {e}", - )] - finally: - await cleanup_mcp_contexts(contexts) - # Log results - total_files = sum(len(s.changed_files) for s in scopes) - logger.info(f"Identified {len(scopes)} scopes covering {total_files} files") - return scopes, final_memory - - def _convert_assignments_to_results( - self, - assignments: list[ScopeAssignment], - changed_files_map: dict[str, ChangedFile], - repo: str, - ) -> list[ScopeResult]: - """Convert LLM scope assignments to ScopeResults with proper ChangedFile objects. - - Args: - assignments: Scope assignments from LLM with string file paths - changed_files_map: Map from filename to ChangedFile object - repo: Repo identifier stamped onto every ScopeResult (see ScopeResult.repo) - - Returns: - List of ScopeResult with ChangedFile objects instead of strings - """ - results: list[ScopeResult] = [] - for assignment in assignments: - # Map string paths to ChangedFile objects - changed_files: list[ChangedFile] = [] - for filepath in assignment.changed_files: - if filepath in changed_files_map: - changed_files.append(changed_files_map[filepath]) - else: - logger.warning(f"File '{filepath}' from scope assignment not found in PR changed files") - results.append(ScopeResult( - repo=repo, - subroot=assignment.subroot, - scope_type=assignment.scope_type, - has_changes=assignment.has_changes, - is_dependency=assignment.is_dependency, - confidence=assignment.confidence, - language=assignment.language, - package_manifest=assignment.package_manifest, - changed_files=changed_files, - reason=assignment.reason, - )) - return results - - def forward( - self, - mr: MergeRequest, - repo_path: Path, - is_local: bool = False, - run_id: str | None = None, - review_context: ReviewContext | None = None, - ) -> tuple[list[ScopeResult], ContextMap | None]: - """Identify scopes (sync wrapper).""" - return asyncio.run(self.aforward(mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_context)) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py new file mode 100644 index 0000000..675c4da --- /dev/null +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -0,0 +1,839 @@ +"""Scope resolver module - merged deterministic analysis + LLM fallback. + +This module combines deterministic scope identification with LLM fallback +for ambiguous cases, replacing the previous split between scope_analyzer +and scope_identifier. +""" + +from __future__ import annotations + +import asyncio +import fnmatch +import logging +import os +from pathlib import Path +from typing import TYPE_CHECKING + +import dspy # type: ignore[import-untyped] +from pydantic import BaseModel, Field + +from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.memory.hippocampus import ContextMap, Hippocampus +from codespy.agents.reviewer.models import ( + PackageManifest, + ReviewContext, + ScopeResult, + ScopeType, +) +from codespy.config import get_settings +from codespy.tools.git.client import get_client +from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file + +if TYPE_CHECKING: + from collections.abc import Sequence + +logger = logging.getLogger(__name__) + +# Exact filename matches -> package manager +MANIFEST_FILES: dict[str, str] = { + # Go + "go.mod": "go", + # JavaScript/TypeScript + "package.json": "npm", + # Python + "pyproject.toml": "pip", + "setup.py": "pip", + "setup.cfg": "pip", + "Pipfile": "pip", + # Rust + "Cargo.toml": "cargo", + # Java/Kotlin/Scala + "pom.xml": "maven", + "build.gradle": "gradle", + "build.gradle.kts": "gradle", + "build.sbt": "sbt", + # PHP + "composer.json": "composer", + # Ruby + "Gemfile": "bundler", + # .NET / C# / F# + "Directory.Build.props": "dotnet", + "Directory.Packages.props": "dotnet", + # Swift + "Package.swift": "swift", + # Dart/Flutter + "pubspec.yaml": "pub", + # Elixir + "mix.exs": "mix", + # Clojure + "deps.edn": "clojure", + "project.clj": "leiningen", + # Haskell + "stack.yaml": "stack", + "cabal.project": "cabal", + # OCaml + "dune-project": "dune", + # Zig + "build.zig.zon": "zig", + # Perl + "cpanfile": "cpan", + "Makefile.PL": "cpan", + # R + "DESCRIPTION": "r", + # Helm charts + "Chart.yaml": "helm", +} + +# Glob patterns for manifests that use variable filenames +MANIFEST_GLOBS: dict[str, str] = { + "*.csproj": "dotnet", + "*.fsproj": "dotnet", + "*.vbproj": "dotnet", + "*.sln": "dotnet", + "*.cabal": "cabal", +} + +# Lock file -> manifest mapping +LOCK_TO_MANIFEST: dict[str, str] = { + "go.sum": "go.mod", + "package-lock.json": "package.json", + "yarn.lock": "package.json", + "pnpm-lock.yaml": "package.json", + "poetry.lock": "pyproject.toml", + "uv.lock": "pyproject.toml", + "Pipfile.lock": "Pipfile", + "Cargo.lock": "Cargo.toml", + "Gemfile.lock": "Gemfile", + "composer.lock": "composer.json", + "pubspec.lock": "pubspec.yaml", + "mix.lock": "mix.exs", + "packages.lock.json": "*.csproj", + "paket.lock": "paket.dependencies", +} + + +# Scope indicator directories +SCOPE_INDICATORS: dict[ScopeType, list[str]] = { + ScopeType.LIBRARY: [ + "lib/", "libs/", "libraries/", + "pkg/", "packages/", + "shared/", "common/", "core/", + "modules/", "mod/", + "sdk/", "sdks/", + "components/", + "plugins/", "extensions/", "addons/", + "middleware/", + "framework/", + "utils/", "utilities/", "helpers/", + "internal/", + ], + ScopeType.SERVICE: [ + "services/", "service/", "svc/", + "microservices/", + "api/", "apis/", + "server/", "servers/", + "backend/", "backends/", + "gateway/", "gateways/", + "proxy/", "proxies/", + "workers/", "worker/", + "jobs/", "cron/", "schedulers/", + "consumers/", "producers/", + "handlers/", "endpoints/", + "functions/", "lambdas/", "lambda/", + "cmd/", + ], + ScopeType.APPLICATION: [ + "apps/", "app/", "applications/", + "web/", "www/", + "frontend/", "frontends/", + "client/", "clients/", + "ui/", + "dashboard/", "admin/", + "portal/", "console/", + "mobile/", "native/", + "ios/", "android/", + "electron/", "desktop/", + "site/", "website/", + ], + ScopeType.SCRIPT: [ + "scripts/", "script/", + "bin/", + "tools/", "tooling/", + "hack/", + "make/", + "ci/", "cd/", + ".github/", ".gitlab/", + ".circleci/", ".buildkite/", + ".azure-pipelines/", + "infra/", "infrastructure/", + "terraform/", "tf/", + "pulumi/", + "ansible/", "salt/", + "cloudformation/", + "docker/", + "k8s/", "kubernetes/", + "helm/", "charts/", + "kustomize/", + "deploy/", "deployment/", "deployments/", + "ops/", "devops/", "platform/", + "provisioning/", + "config/", "configs/", "configuration/", + ], +} + +# Flattened set for sparse path derivation +SCOPE_INDICATOR_DIRS = frozenset( + d.rstrip("/") for dirs in SCOPE_INDICATORS.values() for d in dirs +) + + +class ScopeAssignment(BaseModel): + """LLM-friendly scope assignment with string file paths. + + Used for LLM output in the fallback path. + """ + + subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") + scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") + has_changes: bool = Field( + default=False, description="Whether this scope has changed files from PR" + ) + is_dependency: bool = Field( + default=False, description="Whether this scope depends on a changed scope" + ) + confidence: float = Field( + default=0.8, ge=0.0, le=1.0, description="Confidence score for scope identification" + ) + language: str | None = Field(default=None, description="Primary language detected") + package_manifest: PackageManifest | None = Field( + default=None, description="Package manifest info if present" + ) + changed_files: list[str] = Field( + default_factory=list, description="Changed file paths belonging to this scope" + ) + reason: str = Field(description="Explanation for why this scope was identified") + + +class ScopeClassifierSignature(dspy.Signature): + """Assign orphan files to the most appropriate scope given pre-computed candidates and repo structure. + + You are given: + - Pre-identified scope candidates (from deterministic manifest/indicator analysis) + - Orphan files that could not be assigned deterministically + - A directory tree of the repository + + SCOPE IDENTIFICATION FROM FILE PATHS: + Analyze orphan file paths to find the best matching scope: + 1. Extract common directory prefixes from orphan files to find candidate scopes + 2. Look for scope indicator patterns at ANY DEPTH in the path: + - svc/, services/, microservices/ -> service scope + - libs/, packages/, shared/, common/, core/ -> library scope + - apps/, web/, frontend/, mobile/ -> application scope + - scripts/, bin/, tools/, hack/, ci/, .github/, .gitlab/ -> script scope + 3. Examples of nested scope detection: + - File: mono/svc/my-service-v1/internal/handler.go + -> Scope: mono/svc/my-service-v1 ("svc/" indicates service) + - File: platform/packages/auth/src/index.ts + -> Scope: platform/packages/auth ("packages/" indicates library) + 4. Group files by longest common directory prefix containing a scope indicator + + SCOPE TYPE CLASSIFICATION: + These patterns can appear at ANY nesting depth: + - library: Shared code that others import + * Patterns: */libs/*, */packages/*, */shared/*, */common/*, */core/*, */sdk/* + - service: Isolated microservice with APIs + * Patterns: */services/*, */microservices/*, */svc/*, */cmd/* + - application: Standalone app or frontend + * Patterns: */apps/*, */web/*, */frontend/*, */mobile/* + - script: Build/deployment scripts, tooling, infrastructure + * Patterns: */scripts/*, */bin/*, */tools/*, */ci/*, */.github/*, */infra/* + + MONO-REPO AWARENESS: + - Scope indicator directories (packages/, services/, apps/, svc/) can appear at any level + - Prefer the deepest directory that forms a logical boundary + - Use repo_tree to verify directory structure exists + + CRITICAL RULES: + 1. EVERY orphan file must be assigned to exactly ONE scope + 2. Don't create overlapping scopes (parent contains child) + 3. Prefer the most specific scope -- deepest directory that forms a logical boundary + 4. Use "." as scope ONLY when files are truly root-level with no nested structure + 5. Assign orphans to existing candidates when paths are compatible (file is under candidate subroot) + 6. Create new scopes only when orphan files clearly belong to an undiscovered boundary + + OUTPUT: Include ALL files (from candidates AND orphans) in the final scope assignments. + Group files by common directory prefix. Keep reasoning concise. + """ + + candidates: str = dspy.InputField( + desc="Pre-identified scope candidates with files already assigned (one per line)" + ) + orphan_files: list[str] = dspy.InputField( + desc="File paths that could not be assigned to any candidate scope" + ) + repo_tree: str = dspy.InputField(desc="Directory tree of repo (depth=6)") + mr_title: str = dspy.InputField(desc="PR title for context") + mr_description: str = dspy.InputField(desc="PR description for context") + + scopes: list[ScopeAssignment] = dspy.OutputField( + desc="Final scope assignments including all files from candidates and resolved orphans" + ) + + +def derive_sparse_paths(changed_files: list[str]) -> list[str]: + """Derive minimal sparse checkout paths from changed files. + + Args: + changed_files: List of changed file paths + + Returns: + List of sparse paths for git sparse-checkout + """ + scope_roots: set[str] = set() + + for filepath in changed_files: + parts = filepath.split("/") + if len(parts) <= 1: + continue # Root-level file, handled by "/*" below + + # Strategy A: Find scope indicator and take the next directory + found_indicator = False + for i, part in enumerate(parts[:-1]): # Skip filename + if part.lower() in SCOPE_INDICATOR_DIRS and i + 1 < len(parts) - 1: + scope_root = "/".join(parts[:i + 2]) + "/" + scope_roots.add(scope_root) + found_indicator = True + break + + # Strategy B: No indicator found -- use depth-2 prefix + if not found_indicator: + depth = min(2, len(parts) - 1) + scope_roots.add("/".join(parts[:depth]) + "/") + + # Always include root-level files for root manifests + paths = sorted(scope_roots) + paths.append("/*") + + return paths + + +class ScopeResolver(dspy.Module): + """Deterministic scope resolver with LLM fallback for ambiguous cases.""" + + def __init__(self) -> None: + """Initialize the scope resolver.""" + super().__init__() + self._cost_tracker = get_cost_tracker() + self._settings = get_settings() + + async def _ensure_repo( + self, mr: MergeRequest, repo_path: Path, is_local: bool + ) -> None: + """Clone repo programmatically if not already on disk. + + Args: + mr: The merge request + repo_path: Path where repo should be cloned + is_local: If True, skip cloning (repo already on disk) + """ + if is_local: + logger.debug("Local review - skipping clone") + return + + if repo_path.exists() and (repo_path / ".git").exists(): + logger.debug("Repo already cloned at %s", repo_path) + return + + changed_file_paths = [f.filename for f in mr.changed_files] + sparse_paths = derive_sparse_paths(changed_file_paths) + logger.info("Sparse checkout paths: %s", sparse_paths) + + # Build a dummy URL to get the right client + if mr.platform == "gitlab": + gitlab_url = self._settings.gitlab_url.rstrip("/") + dummy_url = f"{gitlab_url}/{mr.repo_owner}/{mr.repo_name}/-/merge_requests/1" + else: + dummy_url = f"https://github.com/{mr.repo_owner}/{mr.repo_name}/pull/1" + + client = get_client(dummy_url, self._settings) + logger.info( + "Cloning %s/%s@%s...", mr.repo_owner, mr.repo_name, mr.head_sha[:8] + ) + + client.clone_repository( + owner=mr.repo_owner, + repo_name=mr.repo_name, + ref=mr.head_sha, + target_path=repo_path, + depth=1, + sparse_paths=sparse_paths, + ) + logger.info("Clone complete: %s", repo_path) + + def _resolve( + self, repo_path: Path, changed_files: list[ChangedFile], repo: str + ) -> tuple[list[ScopeResult], list[ChangedFile]]: + """Run deterministic scope resolution. + + Args: + repo_path: Path to the cloned repository + changed_files: List of changed files + repo: Repo identifier + + Returns: + Tuple of (active scopes, orphan files) + """ + excluded_dirs = self._settings.excluded_directories + manifests = self._discover_manifests(repo_path, excluded_dirs) + + # Build ScopeResult per manifest + scopes: dict[str, ScopeResult] = {} + for manifest_dir, (pkg_mgr, manifest_filename) in manifests.items(): + subroot = str(manifest_dir) if str(manifest_dir) != "." else "." + lock_file = self._find_lock_file(repo_path, manifest_dir, manifest_filename) + scope_type = self._classify_scope_type(subroot, manifest_filename) + changed_paths = {f.filename for f in changed_files} + deps_changed = self._dependencies_changed(manifest_dir, manifest_filename, lock_file, changed_paths) + manifest_path = str(manifest_dir / manifest_filename) if manifest_dir != Path(".") else manifest_filename + + scopes[subroot] = ScopeResult( + repo=repo, + subroot=subroot, + scope_type=scope_type, + confidence=0.9, + package_manifest=PackageManifest( + manifest_path=manifest_path, + lock_file_path=str(lock_file) if lock_file else None, + package_manager=pkg_mgr, + dependencies_changed=deps_changed, + ), + reason=f"manifest {manifest_filename} at {subroot}/", + ) + + # Add scope-indicator-based scopes for uncovered paths + for file in changed_files: + indicator_type, indicator_path = self._find_scope_indicator(file.filename) + if indicator_path and indicator_type and indicator_path not in scopes: + scopes[indicator_path] = ScopeResult( + repo=repo, + subroot=indicator_path, + scope_type=indicator_type, + confidence=0.9, + reason="scope indicator in path", + ) + + # Assign files to deepest matching scope + orphans = self._assign_files(list(scopes.values()), changed_files) + + # Return only scopes that have files + active_scopes = [s for s in scopes.values() if s.changed_files] + return active_scopes, orphans + + def _discover_manifests( + self, repo_path: Path, excluded_dirs: list[str] + ) -> dict[Path, tuple[str, str]]: + """Discover all package manifests in the repo. + + Args: + repo_path: Path to the repository root + excluded_dirs: List of directory names to exclude from scanning + + Returns: + Dict mapping manifest directory -> (package manager, filename) + """ + manifests: dict[Path, tuple[str, str]] = {} + excluded_set = set(excluded_dirs) + + for root, dirs, files in os.walk(repo_path): + # Skip excluded and hidden directories + dirs[:] = [d for d in dirs if d not in excluded_set and not d.startswith(".")] + + for filename in files: + # Check exact matches + if filename in MANIFEST_FILES: + manifest_path = Path(root) / filename + rel_path = manifest_path.relative_to(repo_path) + manifests[rel_path.parent] = (MANIFEST_FILES[filename], filename) + continue + + # Check glob patterns + for pattern, pkg_mgr in MANIFEST_GLOBS.items(): + if fnmatch.fnmatch(filename, pattern): + manifest_path = Path(root) / filename + rel_path = manifest_path.relative_to(repo_path) + manifests[rel_path.parent] = (pkg_mgr, filename) + break + + return manifests + + def _classify_scope_type(self, subroot: str, manifest_filename: str | None) -> ScopeType: + """Classify scope type from path indicators. + + Args: + subroot: Scope root path + manifest_filename: Manifest filename (optional) + + Returns: + ScopeType classification + """ + subroot_lower = subroot.lower() + + # Check for script indicators first (most specific) + for indicator in SCOPE_INDICATORS[ScopeType.SCRIPT]: + if indicator.rstrip("/") in subroot_lower.split("/"): + return ScopeType.SCRIPT + + # Check for service indicators + for indicator in SCOPE_INDICATORS[ScopeType.SERVICE]: + if indicator.rstrip("/") in subroot_lower.split("/"): + return ScopeType.SERVICE + + # Check for application indicators + for indicator in SCOPE_INDICATORS[ScopeType.APPLICATION]: + if indicator.rstrip("/") in subroot_lower.split("/"): + return ScopeType.APPLICATION + + # Check for library indicators + for indicator in SCOPE_INDICATORS[ScopeType.LIBRARY]: + if indicator.rstrip("/") in subroot_lower.split("/"): + return ScopeType.LIBRARY + + # Default based on manifest type + if manifest_filename: + if manifest_filename == "Chart.yaml": + return ScopeType.SERVICE # Helm charts are deployable + if manifest_filename in ("Dockerfile", "docker-compose.yml"): + return ScopeType.SCRIPT + + # Fallback: library if it has a manifest, application otherwise + return ScopeType.LIBRARY if manifest_filename else ScopeType.APPLICATION + + def _find_lock_file( + self, repo_path: Path, manifest_dir: Path, manifest_filename: str + ) -> Path | None: + """Find the lock file corresponding to a manifest. + + Args: + repo_path: Path to the repository root + manifest_dir: Directory containing manifest + manifest_filename: Name of manifest file + + Returns: + Path to lock file or None + """ + search_dir = repo_path / manifest_dir + if not search_dir.exists(): + return None + + for lock_file, manifest_pattern in LOCK_TO_MANIFEST.items(): + if fnmatch.fnmatch(manifest_filename, manifest_pattern): + lock_path = search_dir / lock_file + if lock_path.exists(): + return lock_path.relative_to(repo_path) + + return None + + def _dependencies_changed( + self, + manifest_dir: Path, + manifest_filename: str, + lock_file: Path | None, + changed_paths: set[str], + ) -> bool: + """Check if manifest or lock file was changed. + + Args: + manifest_dir: Directory containing manifest + manifest_filename: Name of manifest file + lock_file: Path to lock file (optional) + changed_paths: Set of changed file paths + + Returns: + True if dependencies changed + """ + manifest_path = str(manifest_dir / manifest_filename) if manifest_dir != Path(".") else manifest_filename + if manifest_path in changed_paths: + return True + if lock_file and str(lock_file) in changed_paths: + return True + return False + + def _assign_files( + self, scopes: list[ScopeResult], changed_files: list[ChangedFile] + ) -> list[ChangedFile]: + """Assign each changed file to its deepest matching scope. + + Args: + scopes: List of scope results + changed_files: Changed files to assign + + Returns: + List of orphan files that couldn't be assigned + """ + # Sort by depth (deepest first) for greedy assignment + sorted_scopes = sorted(scopes, key=lambda s: s.subroot.count("/"), reverse=True) + orphans: list[ChangedFile] = [] + + for file in changed_files: + assigned = False + for scope in sorted_scopes: + prefix = scope.subroot + "/" if scope.subroot != "." else "" + if file.filename.startswith(prefix) or (scope.subroot == "." and "/" not in file.filename): + scope.changed_files.append(file) + scope.has_changes = True + assigned = True + break + if not assigned: + orphans.append(file) + + return orphans + + def _find_scope_indicator(self, filepath: str) -> tuple[ScopeType | None, str]: + """Find scope indicator in file path. + + Args: + filepath: File path to analyze + + Returns: + Tuple of (scope type, scope root path) or (None, "") + """ + parts = filepath.split("/") + + for i, part in enumerate(parts[:-1]): # Skip filename + part_lower = part.lower() + + # Check each scope type + for scope_type, indicators in SCOPE_INDICATORS.items(): + for indicator in indicators: + indicator_name = indicator.rstrip("/") + if part_lower == indicator_name: + # Scope root is one level past the indicator + if i + 1 < len(parts) - 1: + scope_root = "/".join(parts[:i + 2]) + return scope_type, scope_root + + return None, "" + + def _build_repo_tree( + self, repo_path: Path, max_depth: int = 6, max_lines: int = 200 + ) -> str: + """Build a string representation of the repo tree. + + Args: + repo_path: Path to the repository root + max_depth: Maximum depth to traverse + max_lines: Maximum lines to return + + Returns: + Tree string for LLM context + """ + lines: list[str] = [] + excluded_dirs = self._settings.excluded_directories + + for root, dirs, files in os.walk(repo_path): + rel_root = Path(root).relative_to(repo_path) + depth = len(rel_root.parts) if str(rel_root) != "." else 0 + + if depth > max_depth: + dirs[:] = [] + continue + + dirs[:] = [d for d in dirs if d not in excluded_dirs and not d.startswith(".")] + + indent = " " * depth + dir_name = Path(root).name if depth > 0 else "." + + manifest_markers = [] + for f in files: + if f in MANIFEST_FILES: + manifest_markers.append(f) + for pattern in MANIFEST_GLOBS: + if fnmatch.fnmatch(f, pattern): + manifest_markers.append(f) + + marker_str = f" ({', '.join(manifest_markers)})" if manifest_markers else "" + lines.append(f"{indent}{dir_name}/{marker_str}") + + return "\n".join(lines[:max_lines]) + + async def _resolve_orphans( + self, + scopes: list[ScopeResult], + orphans: list[ChangedFile], + repo_tree: str, + mr: MergeRequest, + review_context: ReviewContext | None, + run_id: str | None, + ) -> list[ScopeResult]: + """Use LLM to resolve ambiguous scope assignments. + + Args: + scopes: Already-resolved scope results + orphans: Orphan files that couldn't be assigned + repo_tree: Directory tree for context + mr: The merge request + review_context: Optional review context with memory + run_id: Pipeline run identifier + + Returns: + List of ScopeResult with LLM-resolved assignments + """ + # Build candidates string from already-resolved scopes + candidates_str = "\n".join( + f"- {s.subroot} ({s.scope_type.value}): " + f"files=[{', '.join(f.filename for f in s.changed_files)}]" + for s in scopes + ) + + predictor = dspy.Predict(ScopeClassifierSignature) + mem: Hippocampus | None = None + + async with SignatureContext("scope", self._cost_tracker): + if self._settings.get_memory_enabled("scope") and review_context: + question = ( + f"classify scopes of {review_context.pr_context.repo_slug}: " + f"pull request {review_context.pr_context.mr_number} " + f"{review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) + mem = Hippocampus( + predictor, + budget=self._settings.get_memory_budget("scope"), + max_reflects=self._settings.get_memory_max_reflects("scope"), + question=question, + task_name="scope", + run_id=run_id, + initial_memory=review_context.memory if review_context else None, + ) + result = await mem.aforward( + candidates=candidates_str, + orphan_files=[f.filename for f in orphans], + repo_tree=repo_tree, + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + ) + else: + result = await predictor.acall( + candidates=candidates_str, + orphan_files=[f.filename for f in orphans], + repo_tree=repo_tree, + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + ) + + # Build file map from all known files + all_files = {f.filename: f for s in scopes for f in s.changed_files} + all_files.update({f.filename: f for f in orphans}) + + return self._convert_assignments(result.scopes, all_files, mr.repo_slug) + + def _convert_assignments( + self, + assignments: list[ScopeAssignment], + changed_files_map: dict[str, ChangedFile], + repo: str, + ) -> list[ScopeResult]: + """Convert LLM scope assignments to ScopeResults. + + Args: + assignments: Scope assignments from LLM + changed_files_map: Map from filename to ChangedFile + repo: Repo identifier + + Returns: + List of ScopeResult + """ + results: list[ScopeResult] = [] + for assignment in assignments: + changed_files: list[ChangedFile] = [] + for filepath in assignment.changed_files: + if filepath in changed_files_map: + changed_files.append(changed_files_map[filepath]) + else: + logger.warning( + "File '%s' from scope assignment not found in PR", filepath + ) + results.append( + ScopeResult( + repo=repo, + subroot=assignment.subroot, + scope_type=assignment.scope_type, + has_changes=assignment.has_changes, + is_dependency=assignment.is_dependency, + confidence=assignment.confidence, + language=assignment.language, + package_manifest=assignment.package_manifest, + changed_files=changed_files, + reason=assignment.reason, + ) + ) + return results + + async def aforward( + self, + mr: MergeRequest, + repo_path: Path, + is_local: bool = False, + run_id: str | None = None, + review_context: ReviewContext | None = None, + ) -> tuple[list[ScopeResult], ContextMap | None]: + """Resolve scopes in the repository for the given MR. + + Args: + mr: The merge request to analyze + repo_path: Path to the repository root + is_local: If True, repo is already on disk + run_id: Pipeline run identifier + review_context: Review context with inherited memory + + Returns: + Tuple of (list of ScopeResult, final context map or None) + """ + excluded_dirs = self._settings.excluded_directories + reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] + + if not reviewable_files: + return [], review_context.memory if review_context else None + + repo = mr.repo_slug + + if not self._settings.is_signature_enabled("scope"): + return [ScopeResult( + repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, + has_changes=True, confidence=0.5, changed_files=reviewable_files, + reason="Scope identification disabled", + )], review_context.memory if review_context else None + + try: + await self._ensure_repo(mr, repo_path, is_local) + scopes, orphans = self._resolve(repo_path, reviewable_files, repo) + + if not orphans: + return scopes, review_context.memory if review_context else None + + # LLM fallback for orphans + repo_tree = self._build_repo_tree(repo_path) + scopes = await self._resolve_orphans(scopes, orphans, repo_tree, mr, review_context, run_id) + return scopes, review_context.memory if review_context else None + + except Exception as e: + logger.error("Scope resolution failed: %s", e, exc_info=True) + return [ScopeResult( + repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, + has_changes=True, confidence=0.5, changed_files=reviewable_files, + reason=f"Fallback due to error: {e}", + )], review_context.memory if review_context else None + + def forward( + self, + mr: MergeRequest, + repo_path: Path, + is_local: bool = False, + run_id: str | None = None, + review_context: ReviewContext | None = None, + ) -> tuple[list[ScopeResult], ContextMap | None]: + """Resolve scopes (sync wrapper).""" + return asyncio.run( + self.aforward( + mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_context + ) + ) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index b21ac90..276d1b0 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -28,7 +28,7 @@ Auditor, CodeReviewer, DocReviewer, - ScopeIdentifier, + ScopeResolver, Summarizer, SupplyChainAuditor, ) @@ -49,7 +49,7 @@ def __init__(self, settings: Settings | None = None) -> None: configure_dspy(self.settings) # Initialize all modules - they internally check if their signatures are enabled - self.scope_identifier = ScopeIdentifier() + self.scope_resolver = ScopeResolver() self.code_reviewer = CodeReviewer() self.doc_reviewer = DocReviewer() self.supply_chain_auditor = SupplyChainAuditor() @@ -204,7 +204,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Step 2: Identify scopes (inherits Summarizer memory) is_local = isinstance(config, LocalReviewConfig) logger.info("Identifying code scopes...") - scopes, scope_memory = self.scope_identifier( + scopes, scope_memory = self.scope_resolver( mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_ctx ) for scope in scopes: @@ -279,7 +279,7 @@ def _collect_scoped_files(scopes: list) -> list[ChangedFile]: summarizer operates on the same focused set as the review modules. Args: - scopes: Identified scopes from scope_identifier + scopes: Identified scopes from scope_resolver Returns: De-duplicated list of ChangedFile objects from all scopes diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py new file mode 100644 index 0000000..fa578ac --- /dev/null +++ b/tests/test_scope_resolver.py @@ -0,0 +1,212 @@ +"""Tests for scope_resolver module.""" + +import os +import tempfile +from pathlib import Path + +import pytest + +from codespy.agents.reviewer.models import ScopeType +from codespy.agents.reviewer.modules.scope_resolver import ( + ScopeResolver, + derive_sparse_paths, +) +from codespy.tools.git.models import ChangedFile, FileStatus + + +class TestDeriveSparsePaths: + """Test sparse path derivation.""" + + def test_single_scope_indicator(self): + """Test deriving sparse paths with single scope indicator.""" + changed_files = ["packages/auth/src/index.ts"] + paths = derive_sparse_paths(changed_files) + assert "packages/auth/" in paths + assert "/*" in paths + + def test_multiple_scope_indicators(self): + """Test deriving sparse paths with multiple indicators.""" + changed_files = [ + "mono/svc/api/cmd/main.go", + "packages/auth/src/index.ts", + ] + paths = derive_sparse_paths(changed_files) + assert "mono/svc/api/" in paths + assert "packages/auth/" in paths + assert "/*" in paths + + def test_no_scope_indicator_uses_depth_fallback(self): + """Test fallback to depth-2 when no indicator found.""" + changed_files = ["backend/api/handler.go"] + paths = derive_sparse_paths(changed_files) + assert "backend/api/" in paths + + def test_root_level_file(self): + """Test root-level files don't add extra paths.""" + changed_files = ["README.md", ".github/workflows/ci.yml"] + paths = derive_sparse_paths(changed_files) + # Should still have /* for root manifests + assert "/*" in paths + + def test_deduplication(self): + """Test that duplicate scope roots are deduplicated.""" + changed_files = [ + "packages/auth/src/index.ts", + "packages/auth/src/utils.ts", + "packages/auth/tests/auth.test.ts", + ] + paths = derive_sparse_paths(changed_files) + # Should only have one packages/auth/ + assert paths.count("packages/auth/") == 1 + + +class TestScopeResolver: + """Test ScopeResolver class.""" + + def test_discover_manifests(self): + """Test manifest discovery.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create a fake repo structure + repo_path = Path(tmpdir) + (repo_path / "packages" / "auth").mkdir(parents=True) + (repo_path / "packages" / "auth" / "package.json").touch() + (repo_path / "services" / "api").mkdir(parents=True) + (repo_path / "services" / "api" / "go.mod").touch() + + resolver = ScopeResolver() + manifests = resolver._discover_manifests(repo_path, []) + + assert len(manifests) == 2 + assert Path("packages/auth") in manifests + assert Path("services/api") in manifests + + def test_single_scope_repo(self): + """Test single-scope repo with root manifest.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "go.mod").touch() + (repo_path / "main.go").touch() + + changed_files = [ + ChangedFile(filename="main.go", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + assert len(scopes) == 1 + assert scopes[0].subroot == "." + assert len(orphans) == 0 + + def test_mono_repo_with_packages(self): + """Test mono-repo with packages/ directory.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "packages" / "auth" / "src").mkdir(parents=True) + (repo_path / "packages" / "auth" / "package.json").touch() + (repo_path / "packages" / "utils").mkdir(parents=True) + (repo_path / "packages" / "utils" / "package.json").touch() + + changed_files = [ + ChangedFile( + filename="packages/auth/src/index.ts", status=FileStatus.MODIFIED + ), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + assert len(scopes) == 1 + assert scopes[0].subroot == "packages/auth" + + def test_orphan_file_no_manifest(self): + """Test file with no manifest becomes orphan.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + # No manifest files + + changed_files = [ + ChangedFile(filename="random/file.py", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + # Should have orphan since no manifest and no scope indicator + assert len(orphans) == 1 + + def test_lock_file_detection(self): + """Test that lock file changes are detected.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "package.json").touch() + (repo_path / "package-lock.json").touch() + + changed_files = [ + ChangedFile( + filename="package-lock.json", status=FileStatus.MODIFIED + ), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + assert len(scopes) == 1 + assert scopes[0].package_manifest is not None + assert scopes[0].package_manifest.dependencies_changed is True + + def test_scope_type_classification(self): + """Test scope type classification from path indicators.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "services" / "api").mkdir(parents=True) + (repo_path / "services" / "api" / "go.mod").touch() + (repo_path / "libs" / "utils").mkdir(parents=True) + (repo_path / "libs" / "utils" / "go.mod").touch() + (repo_path / "apps" / "web").mkdir(parents=True) + (repo_path / "apps" / "web" / "package.json").touch() + + changed_files = [ + ChangedFile( + filename="services/api/main.go", status=FileStatus.MODIFIED + ), + ChangedFile( + filename="libs/utils/helpers.go", status=FileStatus.MODIFIED + ), + ChangedFile( + filename="apps/web/index.ts", status=FileStatus.MODIFIED + ), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + by_subroot = {s.subroot: s for s in scopes} + assert by_subroot["services/api"].scope_type == ScopeType.SERVICE + assert by_subroot["libs/utils"].scope_type == ScopeType.LIBRARY + assert by_subroot["apps/web"].scope_type == ScopeType.APPLICATION + + +class TestConfidenceScoring: + """Test confidence scoring based on manifest types.""" + + def test_strong_manifest_confidence(self): + """Test strong manifests get 0.9 confidence.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "go.mod").touch() + + changed_files = [ + ChangedFile(filename="main.go", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + # Candidate should exist with manifest and 0.9 confidence + assert scopes[0].package_manifest is not None + assert scopes[0].confidence == 0.9 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 6a7bce526a36a8c1dcd3ea0d31ef1cb84a97cc40 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 00:00:27 +0200 Subject: [PATCH 40/79] wip --- src/codespy/agents/memory/hippocampus/hippocampus.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 19f4d5d..ab46801 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -441,7 +441,7 @@ def _make_question(self, inputs: dict) -> str: return format_inputs(inputs, self.budget.max_question_tokens) def _record_mutations( - self, ops: list[Operation], new_ids: list[str] + self, ops: list[Operation], new_ids: list[str], pre_map: ContextMap ) -> list[Mutation]: """Build Mutation records from operations and the new IDs generated by apply(). @@ -451,6 +451,8 @@ def _record_mutations( Args: ops: Cartographer operations (ADD/DELETE/REPLACE). new_ids: IDs of items created by apply() in the same order as ADD ops. + pre_map: Context map state before apply() — used to look up + previous content for DELETE/REPLACE. Returns: List of Mutation records for this step. @@ -459,7 +461,7 @@ def _record_mutations( add_indices: list[int] = [] for op in ops: if op.type == OpType.DELETE and op.item_id: - found = self.cmap.find_item(op.item_id) + found = pre_map.find_item(op.item_id) if found: section, old_item = found mutations.append( @@ -473,7 +475,7 @@ def _record_mutations( ) ) elif op.type == OpType.REPLACE and op.item_id and op.content: - found = self.cmap.find_item(op.item_id) + found = pre_map.find_item(op.item_id) if found: section, old_item = found mutations.append( @@ -536,8 +538,9 @@ def _distill(self, trajectory: str, question: str) -> None: ops = list(edits.operations or []) if ops: + pre_map = self.cmap self.cmap, new_ids = self.cmap.apply(ops) - mutations = self._record_mutations(ops, new_ids) + mutations = self._record_mutations(ops, new_ids, pre_map) self._mutations.extend(mutations) for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 From 88a335f7d73758ca598d0e7f9c346704358c20c1 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 00:21:17 +0200 Subject: [PATCH 41/79] wip --- src/codespy/agents/reviewer/modules/doc_extractor.py | 3 +++ src/codespy/agents/reviewer/modules/doc_reviewer.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/codespy/agents/reviewer/modules/doc_extractor.py b/src/codespy/agents/reviewer/modules/doc_extractor.py index 81ddc6c..1e63ed9 100644 --- a/src/codespy/agents/reviewer/modules/doc_extractor.py +++ b/src/codespy/agents/reviewer/modules/doc_extractor.py @@ -59,6 +59,9 @@ def extract_documentation(scope_root: Path) -> str: Concatenated documentation with ``=== filename ===`` headers, or empty string if no documentation exists. """ + if not scope_root.exists(): + logger.debug("Scope root does not exist, skipping: %s", scope_root) + return "" fs = FileSystem(scope_root, create_if_missing=False) # One tree scan — depth 2 covers root files + immediate subdirs. diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index efd9a66..5908f58 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -145,6 +145,11 @@ async def aforward( ) for scope in changed_scopes: scope_root = resolve_scope_root(repo_path, scope.subroot) + if not scope_root.exists(): + logger.debug( + f" Scope directory does not exist (deleted/moved files): {scope.subroot}" + ) + continue # Step 1: Extract documentation (deterministic — no LLM) logger.info(f" Doc extraction: scope {scope.subroot}") try: From 92f181a9be967b78f4321eefee71762065580b4f Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 13:23:02 +0200 Subject: [PATCH 42/79] wip --- .../agents/memory/hippocampus/hippocampus.py | 6 +- .../agents/reviewer/modules/auditor.py | 14 ++++- .../agents/reviewer/modules/scope_resolver.py | 40 ++++++++++++- .../agents/reviewer/modules/summarizer.py | 14 +++++ src/codespy/agents/reviewer/reviewer.py | 56 ++++++++----------- 5 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index ab46801..f7f1e9f 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -284,7 +284,7 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: self._distill_step = 0 - def _episode_file_path(self, dir: str, index: int = 0) -> str: + def episode_file_path(self, dir: str, index: int = 0) -> str: """Build the full episode file path from a directory. Prepends the ``episodes`` root and appends a hidden ``.codespy`` @@ -343,7 +343,7 @@ def end_episode( return self._finalize_episode(artifacts) if store is not None and dir is not None: - _save_episode(store, self._episode_file_path(dir, self._episode_index), self.episode) + _save_episode(store, self.episode_file_path(dir, self._episode_index), self.episode) self._episode_index += 1 async def aend_episode( @@ -372,7 +372,7 @@ async def aend_episode( return await asyncio.to_thread(self._finalize_episode, artifacts) if store is not None and dir is not None: - path = self._episode_file_path(dir, self._episode_index) + path = self.episode_file_path(dir, self._episode_index) await asyncio.to_thread(_save_episode, store, path, self.episode) self._episode_index += 1 diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 4e35f4e..fa2aef9 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -1,7 +1,7 @@ """Auditor module — assesses code quality and provides recommendation after reviews.""" import logging -from typing import Sequence +from typing import TYPE_CHECKING, Sequence import dspy @@ -12,6 +12,9 @@ from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile +if TYPE_CHECKING: + from codespy.agents.reviewer.models import ScopeResult + logger = logging.getLogger(__name__) @@ -57,6 +60,7 @@ def forward( changed_files: Sequence[ChangedFile], all_issues: Sequence[Issue], run_id: str | None = None, + scopes: list["ScopeResult"] | None = None, ) -> tuple[str, str]: """Assess quality and recommend action. @@ -65,6 +69,7 @@ def forward( changed_files: In-scope reviewable files all_issues: All issues found during review run_id: Pipeline run identifier + scopes: List of resolved scopes for per-scope episode persistence Returns: Tuple of (quality_assessment, recommendation) @@ -119,4 +124,11 @@ def forward( all_issues=list(all_issues), ) + # Persist episode at each scope location if memory is enabled and scopes are provided + if mem is not None and scopes: + store = get_memory_store(self._settings) + for scope in scopes: + path = mem.episode_file_path(scope.scope_path()) + mem.save_episode(store, path) + return result.quality_assessment, result.recommendation diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 675c4da..d514220 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -26,6 +26,7 @@ ScopeType, ) from codespy.config import get_settings +from codespy.config_memory import get_memory_store from codespy.tools.git.client import get_client from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file @@ -34,6 +35,28 @@ logger = logging.getLogger(__name__) + +def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: + """Compute the deepest common ancestor directory across all scope subroots. + + Args: + scopes: List of scope results + repo_slug: Repository slug for fallback path + + Returns: + Deepest common ancestor path (e.g., "/repo/scope/subroot/") + """ + subroots = [s.subroot for s in scopes] + if not subroots or any(sr in (".", "") for sr in subroots): + return f"/{repo_slug}/" + try: + common = os.path.commonpath(subroots) + except ValueError: + common = "" + if not common or common == ".": + return f"/{repo_slug}/" + return f"/{repo_slug}/{common.strip('/')}/" + # Exact filename matches -> package manager MANIFEST_FILES: dict[str, str] = { # Go @@ -724,7 +747,22 @@ async def _resolve_orphans( all_files = {f.filename: f for s in scopes for f in s.changed_files} all_files.update({f.filename: f for f in orphans}) - return self._convert_assignments(result.scopes, all_files, mr.repo_slug) + final_scopes = self._convert_assignments(result.scopes, all_files, mr.repo_slug) + + # Persist episode at deepest common folder when LLM fallback was used and memory is enabled + if mem is not None: + common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) + scope_desc = "\n".join( + f"- {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" + for s in final_scopes + ) + await mem.aend_episode( + get_memory_store(self._settings), + common_dir, + artifacts={"scopes": scope_desc}, + ) + + return final_scopes def _convert_assignments( self, diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 11de9b4..37928c6 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -1,6 +1,7 @@ """PR summarizer module — produces a concise summary before scope identification.""" import logging +from typing import TYPE_CHECKING import dspy @@ -9,6 +10,9 @@ from codespy.config import get_settings from codespy.config_memory import get_memory_store +if TYPE_CHECKING: + from codespy.agents.reviewer.models import ScopeResult + logger = logging.getLogger(__name__) @@ -51,6 +55,7 @@ def forward( patches: str, repo_slug: str, run_id: str | None = None, + scopes: list["ScopeResult"] | None = None, ) -> tuple[str, ContextMap | None]: """Generate a PR summary. @@ -62,6 +67,7 @@ def forward( patches: Unified diff patches showing code changes repo_slug: Host-qualified repo slug for episode path run_id: Pipeline run identifier + scopes: List of resolved scopes for per-scope episode persistence Returns: Tuple of (summary string, final context map or None) @@ -109,4 +115,12 @@ def forward( logger.info(f"PR summary: {result.summary[:80]}...") # Return final context map when memory is enabled final_memory = mem.cmap.model_copy(deep=True) if mem else None + + # Persist episode at each scope location if memory is enabled and scopes are provided + if mem is not None and scopes: + store = get_memory_store(self._settings) + for scope in scopes: + path = mem.episode_file_path(scope.scope_path()) + mem.save_episode(store, path) + return result.summary, final_memory diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 276d1b0..333d73e 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -178,33 +178,17 @@ def forward(self, config: ReviewConfig) -> ReviewResult: else: raise ValueError(f"Invalid config type: {type(config)}") - # Step 1: Run Summarizer (before scope identification) - changed_file_paths = [f.filename for f in mr.changed_files] - patches = build_patches(mr.changed_files) - pr_summary, summarizer_memory = self.summarizer( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - mr_number=mr.number, - changed_file_paths=changed_file_paths, - patches=patches, - repo_slug=mr.repo_slug, - run_id=run_id, - ) - - # Build PRContext and ReviewContext after summarizer runs + # Step 1: Identify scopes FIRST + is_local = isinstance(config, LocalReviewConfig) + logger.info("Identifying code scopes...") pr_context = PRContext( repo_slug=mr.repo_slug, mr_number=mr.number, mr_title=mr.title, - summary=pr_summary, + summary=mr.title, # Use title as placeholder since summary hasn't run ) - # Summarizer is first stage - no inherited memory yet - review_ctx = ReviewContext(pr_context=pr_context, memory=summarizer_memory) - - # Step 2: Identify scopes (inherits Summarizer memory) - is_local = isinstance(config, LocalReviewConfig) - logger.info("Identifying code scopes...") - scopes, scope_memory = self.scope_resolver( + review_ctx = ReviewContext(pr_context=pr_context, memory=None) + scopes, _ = self.scope_resolver( mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_ctx ) for scope in scopes: @@ -216,14 +200,25 @@ def forward(self, config: ReviewConfig) -> ReviewResult: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") - - # Update ReviewContext with Scope Identifier's memory for downstream modules - review_ctx = ReviewContext(pr_context=pr_context, memory=scope_memory) - # Compact patches: expand context to function bodies for better review context logger.info("Compacting patches to function boundaries...") + changed_file_paths = [f.filename for f in mr.changed_files] + patches = build_patches(mr.changed_files) compact_patches(scopes, repo_path) - + # Step 2: Run Summarizer (now receives scopes for per-scope episode persistence) + pr_summary, summarizer_memory = self.summarizer( + mr_title=mr.title, + mr_description=mr.body or "No description provided.", + mr_number=mr.number, + changed_file_paths=changed_file_paths, + patches=patches, + repo_slug=mr.repo_slug, + run_id=run_id, + scopes=scopes, + ) + # Enrich review_ctx with actual summary and memory from summarizer + pr_context.summary = pr_summary + review_ctx = ReviewContext(pr_context=pr_context, memory=summarizer_memory) # Step 3: Run review modules concurrently via asyncio.gather (inherit Scope Identifier memory) module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") @@ -231,12 +226,10 @@ def forward(self, config: ReviewConfig) -> ReviewResult: self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, review_context=review_ctx) ) logger.info(f"Found {len(all_issues)} issues") - # Merge parallel context maps for Auditor maps_to_merge = [m for m in parallel_memories.values() if m is not None] - merged_memory = ContextMap.merge(*maps_to_merge) if maps_to_merge else scope_memory + merged_memory = ContextMap.merge(*maps_to_merge) if maps_to_merge else summarizer_memory review_ctx = ReviewContext(pr_context=pr_context, memory=merged_memory) - # Step 4: Run Audit (inherits merged memory from parallel modules) scoped_files = self._collect_scoped_files(scopes) logger.info( @@ -248,11 +241,10 @@ def forward(self, config: ReviewConfig) -> ReviewResult: changed_files=scoped_files, all_issues=all_issues, run_id=run_id, + scopes=scopes, ) - # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() - return ReviewResult( mr_number=mr.number, mr_title=mr.title, From 33a77770b582ba09ec83c4e24474767455014131 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 13:39:59 +0200 Subject: [PATCH 43/79] wip --- src/codespy/agents/cost_tracker.py | 12 +++++----- .../agents/memory/hippocampus/budget.py | 8 +++++-- src/codespy/tools/git/patch_utils.py | 24 ++++++++++--------- .../parsers/treesitter/extractors/cpp.py | 2 +- .../treesitter/extractors/ripgrep_fallback.py | 4 ++-- .../parsers/treesitter/extractors/ruby.py | 2 +- .../tools/storage/filesystem/server.py | 14 ++++++++--- src/codespy/tools/storage/s3/client.py | 12 ++++++++++ src/codespy/tools/storage/s3/server.py | 16 +++++++++---- 9 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index 9a1ad1c..c1bc21b 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -203,9 +203,9 @@ def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) Tuple of (total_cost, total_tokens, call_count) """ total_cost = 0.0 - total_tokens = 0.0 + total_tokens = 0 call_count = 0 - + for entry in entries: if not isinstance(entry, dict): continue @@ -217,12 +217,12 @@ def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) # Get tokens from usage usage = entry.get("usage") if isinstance(usage, dict): - total_tokens += _as_number(usage.get("prompt_tokens")) - total_tokens += _as_number(usage.get("completion_tokens")) + total_tokens += int(_as_number(usage.get("prompt_tokens"))) + total_tokens += int(_as_number(usage.get("completion_tokens"))) call_count += 1 - - return total_cost, int(total_tokens), call_count + + return total_cost, total_tokens, call_count class SignatureContext: diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 48c10e1..b4bf238 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -143,8 +143,12 @@ def _format_step(i: int, entry: dict) -> str: parts = [f"--- Step {i + 1} ---"] if entry.get("reasoning"): parts.append(f"Reasoning: {entry['reasoning']}") - parts.append(f"Code:\n{entry['code']}") - parts.append(f"Output:\n{entry['output']}") + code = entry.get("code", "") + if code: + parts.append(f"Code:\n{code}") + output = entry.get("output", "") + if output: + parts.append(f"Output:\n{output}") return "\n".join(parts) diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index 99ab90f..61e7190 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -406,20 +406,24 @@ def _rebuild_patch( new_hunk_lines = [] # Add context lines before the original hunk (from expansion_start to hunk_start_new - 1) + pre_context_lines = [] for line_num in range(expansion_start, hunk_start_new): if line_num <= len(source_lines): - new_hunk_lines.append(f" {source_lines[line_num - 1]}") + pre_context_lines.append(f" {source_lines[line_num - 1]}") + new_hunk_lines.extend(pre_context_lines) - # Add all original hunk lines (context + changes). - # No overlap with expansion context: pre-expansion ends at hunk_start_new, - # post-expansion begins at hunk_end_new + 1, so interior context is unique. - original_lines = original_hunk.get("lines", []) - new_hunk_lines.extend(original_lines) + # Add diff lines from all constituent hunks (merged_hunks if present, else original_hunk) + merged_sub_hunks = merged_hunk.get("merged_hunks", [original_hunk]) + for sub_hunk in merged_sub_hunks: + sub_lines = sub_hunk.get("lines", []) + new_hunk_lines.extend(sub_lines) # Add context lines after the original hunk (from hunk_end_new + 1 to expansion_end) + post_context_lines = [] for line_num in range(hunk_end_new + 1, expansion_end + 1): if line_num <= len(source_lines): - new_hunk_lines.append(f" {source_lines[line_num - 1]}") + post_context_lines.append(f" {source_lines[line_num - 1]}") + new_hunk_lines.extend(post_context_lines) # Calculate new hunk header counts # For the new file: count context lines and additions @@ -428,10 +432,8 @@ def _rebuild_patch( if line.startswith(" ") or line.startswith("+"): new_file_count += 1 - # For the old file: we approximate by using the ratio of original change - # This is a simplification; the old file line numbers would need full reconstruction - # We use the original old_count as a reasonable approximation - old_count = original_hunk.get("old_count", new_file_count) + # For the old file: count context (" ") and deletion ("-") lines + old_count = sum(1 for line in new_hunk_lines if line.startswith(" ") or line.startswith("-")) # Build new header new_header = f"@@ -{expansion_start},{old_count} +{expansion_start},{new_file_count} @@" diff --git a/src/codespy/tools/parsers/treesitter/extractors/cpp.py b/src/codespy/tools/parsers/treesitter/extractors/cpp.py index 035ad5e..e81bac7 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/cpp.py +++ b/src/codespy/tools/parsers/treesitter/extractors/cpp.py @@ -118,7 +118,7 @@ def _is_in_class_context(self, node: Any) -> bool: """Check if function is inside a class/struct context.""" current = node while current: - if current.type in ("class_specifier", "struct_specifier", "namespace_definition"): + if current.type in ("class_specifier", "struct_specifier"): return True current = current.parent return False diff --git a/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py index 11e258e..dd245f8 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +++ b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py @@ -90,12 +90,12 @@ class RipgrepHeuristicsExtractor: ( "sql", re.compile( + r"(?i)" # case-insensitive inline flag r"^[\s]*" # leading whitespace r"(?:CREATE\s+(?:OR\s+REPLACE\s+)?)?" # optional CREATE OR REPLACE r"(?:PROCEDURE|FUNCTION|TRIGGER)\s+" # object type r"(?:[\w.]+\s+)?" # optional schema prefix - r"(\w+)", # name - re.IGNORECASE, + r"(\w+)" # name ), ), ] diff --git a/src/codespy/tools/parsers/treesitter/extractors/ruby.py b/src/codespy/tools/parsers/treesitter/extractors/ruby.py index 30f6319..a7e1d80 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/ruby.py +++ b/src/codespy/tools/parsers/treesitter/extractors/ruby.py @@ -110,7 +110,7 @@ def _extract_ruby_params(self, node: Any, source: bytes) -> list[str]: param_name = self._get_node_text(child, source) if param_name and param_name not in ("(", ")", ",", "|", "&"): params.append(param_name) - elif child.type in ("optional_parameter", "keyword_parameter"): + elif child.type == "optional_parameter": name_node = child.child_by_field_name("name") if name_node: param_name = self._get_node_text(name_node, source) diff --git a/src/codespy/tools/storage/filesystem/server.py b/src/codespy/tools/storage/filesystem/server.py index 3b6fcad..8ff53c5 100644 --- a/src/codespy/tools/storage/filesystem/server.py +++ b/src/codespy/tools/storage/filesystem/server.py @@ -18,6 +18,9 @@ mcp = FastMCP("filesystem") _fs: FileSystem | None = None +# Manual cache for read_file to skip caching error results +_read_file_cache: dict[tuple[str, int, int | None], tuple] = {} + def _get_fs() -> FileSystem: """Get the FileSystem instance, raising if not initialized.""" @@ -26,11 +29,16 @@ def _get_fs() -> FileSystem: return _fs -@lru_cache(maxsize=256) def _read_file_cached(path: str, max_bytes: int, max_lines: int | None) -> tuple: - """Cached version of read_file.""" + """Cached version of read_file that doesn't cache error results.""" + key = (path, max_bytes, max_lines) + if key in _read_file_cache: + return _read_file_cache[key] result = _get_fs().read_file(path, max_bytes, max_lines) - return tuple(sorted(result.model_dump().items())) + dumped = tuple(sorted(result.model_dump().items())) + if not result.error: + _read_file_cache[key] = dumped + return dumped @mcp.tool() diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index c6afcf5..47ee851 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -361,6 +361,18 @@ def delete_file(self, path: str) -> OperationResult: error="Cannot delete: path resolves to bucket root", ) + # Check existence first (matches filesystem semantics) + try: + self._s3.head_object(Bucket=self.bucket, Key=file_path) + except self._s3.exceptions.ClientError as e: + if self._client_error_code(e) == "404": + return OperationResult( + success=False, + path=file_path, + error=f"File not found: {path}", + ) + raise + try: self._s3.delete_object(Bucket=self.bucket, Key=file_path) return OperationResult( diff --git a/src/codespy/tools/storage/s3/server.py b/src/codespy/tools/storage/s3/server.py index 9e4431a..07f6ec3 100644 --- a/src/codespy/tools/storage/s3/server.py +++ b/src/codespy/tools/storage/s3/server.py @@ -17,6 +17,9 @@ mcp = FastMCP("s3") _client: S3Client | None = None +# Manual cache for read_file to skip caching error results +_read_file_cache: dict[tuple[str, int, int | None], tuple] = {} + def _get_client() -> S3Client: """Get the S3Client instance, raising if not initialized.""" @@ -121,11 +124,16 @@ def get_tree(path: str = "", max_depth: int = 3, include_hidden: bool = False) - return _get_tree_cached(path, max_depth, include_hidden) -@lru_cache(maxsize=256) def _read_file_cached(path: str, max_bytes: int, max_lines: int | None) -> tuple: - """Cached version of read_file.""" + """Cached version of read_file that doesn't cache error results.""" + key = (path, max_bytes, max_lines) + if key in _read_file_cache: + return _read_file_cache[key] result = _get_client().read_file(path, max_bytes, max_lines) - return tuple(sorted(result.model_dump().items())) + dumped = tuple(sorted(result.model_dump().items())) + if not result.error: + _read_file_cache[key] = dumped + return dumped @mcp.tool() @@ -163,7 +171,7 @@ def _invalidate_read_caches() -> None: _get_file_info_cached.cache_clear() _list_directory_cached.cache_clear() _get_tree_cached.cache_clear() - _read_file_cached.cache_clear() + _read_file_cache.clear() # ------------------------------------------------------------------ From 6f8ff10d2e6e8e177f000c73c0c00a126e5b12db Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 13:53:42 +0200 Subject: [PATCH 44/79] wip --- src/codespy/agents/reviewer/modules/scope_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index d514220..8ba2729 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -708,7 +708,7 @@ async def _resolve_orphans( for s in scopes ) - predictor = dspy.Predict(ScopeClassifierSignature) + predictor = dspy.ChainOfThought(ScopeClassifierSignature) mem: Hippocampus | None = None async with SignatureContext("scope", self._cost_tracker): From 5cbfcb2818fae239c2f5e6dae97cd8f0045b3b82 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 20:44:09 +0200 Subject: [PATCH 45/79] wip --- src/codespy/agents/dspy_config.py | 11 ++++++++- .../agents/memory/hippocampus/hippocampus.py | 24 ++++++++++++++++--- .../memory/hippocampus/modules/distiller.py | 8 +++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 92bcbbb..06f4ee6 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -137,7 +137,16 @@ def lm_context(name: str): A context manager that scopes the LM to the enclosed block. """ settings = get_settings() - return dspy.context(lm=new_lm(settings, settings.get_llm_config(name))) + llm_config = settings.get_llm_config(name) + lm = new_lm(settings, llm_config) + # Override adapter when this module has a different extraction model + defaults = settings.get_llm_config("default") + if llm_config.extraction_model != defaults.extraction_model: + extraction_lm = new_lm( + settings, llm_config.model_copy(update={"model": llm_config.extraction_model}) + ) + return dspy.context(lm=lm, adapter=TwoStepAdapter(extraction_lm)) + return dspy.context(lm=lm) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index f7f1e9f..59d1c70 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -2,11 +2,15 @@ import asyncio import copy +import logging import uuid from datetime import UTC, datetime import dspy + +logger = logging.getLogger(__name__) + from codespy.agents.memory.hippocampus.budget import ( MemoryBudget, _head_tail_text, @@ -238,8 +242,15 @@ def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None: # Online reflection: None = no limit (always); N = for the first N calls. if (self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects): - self._distill(traj, self._make_question(kwargs)) - self._reflected_count += 1 + try: + self._distill(traj, self._make_question(kwargs)) + self._reflected_count += 1 + except Exception: + logger.warning( + "Online reflection failed for %s; trajectory buffered for end_episode().", + self._task_name, + exc_info=True, + ) def _consolidate(self) -> str | None: """Join buffered trajectories (stage-2 bounded) and distill+apply once. @@ -256,7 +267,14 @@ def _consolidate(self) -> str | None: ) if self.budget.max_trajectory_tokens is not None: combined = _head_tail_text(combined, self.budget.max_trajectory_tokens) - self._distill(combined, self._episode_question or "") + try: + self._distill(combined, self._episode_question or "") + except Exception: + logger.warning( + "Consolidation reflection failed for %s; episode saved without final distill.", + self._task_name, + exc_info=True, + ) return combined def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 68a4cb7..7c122ea 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -101,6 +101,14 @@ class DistillerSig(dspy.Signature): onto the context map schema): context_understanding, domain_constants, context_roadmap, reusable_results, parsing_schema. + Each candidate is a JSON object with exactly these fields: + - section: one of the five section names above + - value: the compact cached content (stay within max_context_item_tokens) + - transferability: what kinds of future questions this would help + - rationale: why this is shared understanding, not a one-off fact + + Do NOT invent extra fields (no "id", no "content", no "name"). + The litmus test for every candidate: "Would a future agent asking a completely DIFFERENT question about this context benefit from knowing this?" From 0b54b339b15ab4d331e242c689a190954dc41ab7 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 21:07:01 +0200 Subject: [PATCH 46/79] wip --- src/codespy/agents/memory/hippocampus/hippocampus.py | 3 ++- src/codespy/agents/reviewer/modules/auditor.py | 1 + src/codespy/tools/git/patch_utils.py | 8 +++++--- src/codespy/tools/parsers/treesitter/extractors/php.py | 3 +-- src/codespy/tools/storage/s3/client.py | 6 ++++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 59d1c70..fd1d10a 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -164,7 +164,8 @@ class for per-field guidance. Resolve one from configuration with module.signature = prepend_context_map(top_sig) for _, pred in module.named_predictors(): if set(pred.signature.input_fields) & module_inputs: - pred.signature = prepend_context_map(pred.signature) + if "context_map" not in pred.signature.input_fields: + pred.signature = prepend_context_map(pred.signature) else: for _, pred in module.named_predictors(): pred.signature = prepend_context_map(pred.signature) diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index fa2aef9..d90f817 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -90,6 +90,7 @@ def forward( ) with SignatureContext("audit", self._cost_tracker): + mem: Hippocampus | None = None if self._settings.get_memory_enabled("audit"): mem = Hippocampus( auditor, diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index 61e7190..aec65bd 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -333,8 +333,8 @@ def _merge_hunks(expanded_hunks: list[dict[str, Any]]) -> list[dict[str, Any]]: if len(expanded_hunks) <= 1: return expanded_hunks - # Sort by expansion start - sorted_hunks = sorted(expanded_hunks, key=lambda h: h.get("expansion_start", 0)) + # Sort by expansion start (fallback to new_start for non-expanded hunks) + sorted_hunks = sorted(expanded_hunks, key=lambda h: h.get("expansion_start", h.get("new_start", 0))) merged = [sorted_hunks[0]] @@ -345,7 +345,9 @@ def _merge_hunks(expanded_hunks: list[dict[str, Any]]) -> list[dict[str, Any]]: last_end = last.get("expansion_end", 0) current_start = hunk.get("expansion_start", 0) - if current_start <= last_end + 1: # Overlapping or adjacent + # Only merge expanded hunks (those with expansion boundaries) + can_merge = "expansion_start" in last and "expansion_start" in hunk + if can_merge and current_start <= last_end + 1: # Overlapping or adjacent # Merge: extend the end if needed last["expansion_end"] = max(last_end, hunk.get("expansion_end", 0)) # Keep track of original hunks for reconstruction diff --git a/src/codespy/tools/parsers/treesitter/extractors/php.py b/src/codespy/tools/parsers/treesitter/extractors/php.py index a154549..784187c 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/php.py +++ b/src/codespy/tools/parsers/treesitter/extractors/php.py @@ -23,7 +23,7 @@ def extract_functions( def visit(n: Any, in_class: bool = False) -> None: if n.type == "function_definition": - func_info = self._extract_function_info(n, file_path, source, in_class) + func_info = self._extract_function_info(n, file_path, source) if func_info: functions.append(func_info) @@ -55,7 +55,6 @@ def _extract_function_info( func_node: Any, file_path: Path, source: bytes, - in_class: bool, ) -> FunctionInfo | None: """Extract function info from a function_definition node.""" name_node = func_node.child_by_field_name("name") diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index 47ee851..eb24a3b 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -241,12 +241,11 @@ def read_file( truncated = False try: - raw: bytes = resp["Body"].read(max_bytes + 1) + raw: bytes = resp["Body"].read() except Exception as e: return Content(path=file_path, error=f"Error reading body: {e}", size=size, content_type=content_type) if len(raw) > max_bytes: - raw = raw[:max_bytes] truncated = True try: @@ -264,6 +263,9 @@ def read_file( total_lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0) + if truncated: + content = content[:max_bytes] + if max_lines is not None: lines = content.split("\n") if len(lines) > max_lines: From 0bff96f914024fcbdf361adac38416b56d0a9391 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 10 Aug 2026 21:58:19 +0200 Subject: [PATCH 47/79] wip --- src/codespy/agents/dspy_config.py | 48 +++++++++++++++- tests/test_dspy_config.py | 91 +++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/test_dspy_config.py diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 06f4ee6..a540218 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -69,6 +69,36 @@ def _supports_reasoning_effort(model: str) -> bool | None: return "reasoning_effort" in params +def _supports_cache_control(model: str) -> bool: + """Whether the model uses explicit Anthropic-style cache_control markers. + + Returns True only for models where the provider expects ``cache_control`` + fields in messages to enable prompt caching. Models with automatic caching + (OpenAI prefix-match, Gemini) or no caching return False — sending markers + to them is either pointless or causes provider errors. + + Detection: ``cache_creation_input_token_cost`` is non-None in LiteLLM's + model database only for Anthropic-style providers that charge separately + for cache writes, which correlates exactly with explicit marker support. + + Falls back to provider-prefix heuristic when model info is unavailable. + """ + try: + info = litellm.get_model_info(model) + if info.get("cache_creation_input_token_cost") is not None: + return True + return False + except Exception: + # Model not in LiteLLM DB (Ollama offline, custom endpoint). + # Fall back to prefix heuristic. + lower = model.lower() + if lower.startswith("anthropic/"): + return True + if lower.startswith("bedrock/") and "anthropic" in lower: + return True + return False + + def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: """Build a ``dspy.LM`` for resolved LLM settings. @@ -114,11 +144,18 @@ def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: ) else: lm_kwargs["reasoning_effort"] = config.reasoning_effort - # Cache system prompts on the provider's servers (Anthropic, OpenAI, Bedrock...) - if settings.enable_prompt_caching: + # Cache system prompts via explicit Anthropic-style cache_control markers. + # Only injected for providers that use explicit markers (Anthropic, Bedrock + # Anthropic); OpenAI/Gemini have automatic caching that needs no markers. + if settings.enable_prompt_caching and _supports_cache_control(config.model): lm_kwargs["cache_control_injection_points"] = [ {"location": "message", "role": "system"} ] + elif settings.enable_prompt_caching: + logger.debug( + f"Prompt caching enabled but model {config.model} does not use " + f"explicit cache_control markers — skipping injection" + ) return dspy.LM(**lm_kwargs) @@ -205,7 +242,12 @@ def configure_dspy(settings: Settings) -> None: # Enable memory-only caching for LLM calls (no disk caching) dspy.configure_cache(enable_memory_cache=True, enable_disk_cache=False, memory_max_entries=10000) - prompt_cache_status = "enabled" if settings.enable_prompt_caching else "disabled" + if not settings.enable_prompt_caching: + prompt_cache_status = "disabled" + elif _supports_cache_control(model): + prompt_cache_status = "enabled (cache_control markers)" + else: + prompt_cache_status = "enabled (provider-automatic, no markers)" logger.info( f"Configured DSPy with model: {model} " f"(TwoStepAdapter with extraction_model={extraction_model}, " diff --git a/tests/test_dspy_config.py b/tests/test_dspy_config.py new file mode 100644 index 0000000..fe6ba51 --- /dev/null +++ b/tests/test_dspy_config.py @@ -0,0 +1,91 @@ +"""Tests for dspy_config model-capability helpers. + +These tests use a standalone copy of the _supports_cache_control function + to avoid heavy import dependencies on dspy, litellm, and other modules. +""" + +from unittest.mock import patch, MagicMock + +import pytest + + +# Standalone copy of the function for isolated testing +def _supports_cache_control(model: str) -> bool: + """Whether the model uses explicit Anthropic-style cache_control markers. + + Returns True only for models where the provider expects ``cache_control`` + fields in messages to enable prompt caching. Models with automatic caching + (OpenAI prefix-match, Gemini) or no caching return False — sending markers + to them is either pointless or causes provider errors. + + Detection: ``cache_creation_input_token_cost`` is non-None in LiteLLM's + model database only for Anthropic-style providers that charge separately + for cache writes, which correlates exactly with explicit marker support. + + Falls back to provider-prefix heuristic when model info is unavailable. + """ + import litellm # noqa: F401 - this is mocked + try: + info = litellm.get_model_info(model) + if info.get("cache_creation_input_token_cost") is not None: + return True + return False + except Exception: + # Model not in LiteLLM DB (Ollama offline, custom endpoint). + # Fall back to prefix heuristic. + lower = model.lower() + if lower.startswith("anthropic/"): + return True + if lower.startswith("bedrock/") and "anthropic" in lower: + return True + return False + + +class TestSupportsCacheControl: + """Tests for _supports_cache_control().""" + + def test_anthropic_direct_supported(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {"cache_creation_input_token_cost": 6.25e-06} + assert _supports_cache_control("anthropic/claude-opus-4-6") is True + + def test_bedrock_anthropic_supported(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {"cache_creation_input_token_cost": 3.75e-06} + assert _supports_cache_control("bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0") is True + + def test_openai_no_explicit_markers(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {"cache_creation_input_token_cost": None} + assert _supports_cache_control("openai/gpt-5") is False + + def test_gemini_no_explicit_markers(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {"cache_creation_input_token_cost": None} + assert _supports_cache_control("gemini/gemini-2.5-pro") is False + + def test_bedrock_non_anthropic_no_markers(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {"cache_creation_input_token_cost": None} + assert _supports_cache_control("bedrock/amazon.titan-text-express-v1") is False + + def test_fallback_anthropic_prefix_when_info_unavailable(self): + with patch("litellm.get_model_info", side_effect=Exception("not found")): + assert _supports_cache_control("anthropic/claude-unknown-model") is True + + def test_fallback_bedrock_anthropic_when_info_unavailable(self): + with patch("litellm.get_model_info", side_effect=Exception("not found")): + assert _supports_cache_control("bedrock/us.anthropic.claude-future-v1") is True + + def test_fallback_ollama_when_info_unavailable(self): + with patch("litellm.get_model_info", side_effect=Exception("not found")): + assert _supports_cache_control("ollama/llama-4-70b") is False + + def test_fallback_unknown_provider_returns_false(self): + with patch("litellm.get_model_info", side_effect=Exception("not found")): + assert _supports_cache_control("custom/my-model") is False + + def test_missing_key_in_info_dict_returns_false(self): + with patch("litellm.get_model_info") as mock: + mock.return_value = {} # Key not present at all + assert _supports_cache_control("some/model") is False From 7611884dffff0b4064b59ce70c673792b6263a91 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 00:38:35 +0200 Subject: [PATCH 48/79] wip --- .../agents/reviewer/modules/scope_resolver.py | 344 +++++++++--------- tests/test_scope_resolver.py | 84 +++++ 2 files changed, 257 insertions(+), 171 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 8ba2729..81c0cce 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -1,8 +1,9 @@ -"""Scope resolver module - merged deterministic analysis + LLM fallback. +"""Scope resolver module - merged deterministic analysis + ReAct agent refinement. -This module combines deterministic scope identification with LLM fallback -for ambiguous cases, replacing the previous split between scope_analyzer -and scope_identifier. +This module combines deterministic scope identification with a ReAct agent +for intelligent refinement. The agent uses filesystem and search tools to +explore the codebase and make informed scope decisions, replacing the +previous ChainOfThought predictor that relied on a static repo tree. """ from __future__ import annotations @@ -12,7 +13,7 @@ import logging import os from pathlib import Path -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import dspy # type: ignore[import-untyped] from pydantic import BaseModel, Field @@ -29,9 +30,7 @@ from codespy.config_memory import get_memory_store from codespy.tools.git.client import get_client from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file - -if TYPE_CHECKING: - from collections.abc import Sequence +from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -145,10 +144,7 @@ def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: "sdk/", "sdks/", "components/", "plugins/", "extensions/", "addons/", - "middleware/", "framework/", - "utils/", "utilities/", "helpers/", - "internal/", ], ScopeType.SERVICE: [ "services/", "service/", "svc/", @@ -161,7 +157,6 @@ def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: "workers/", "worker/", "jobs/", "cron/", "schedulers/", "consumers/", "producers/", - "handlers/", "endpoints/", "functions/", "lambdas/", "lambda/", "cmd/", ], @@ -237,69 +232,59 @@ class ScopeAssignment(BaseModel): reason: str = Field(description="Explanation for why this scope was identified") -class ScopeClassifierSignature(dspy.Signature): - """Assign orphan files to the most appropriate scope given pre-computed candidates and repo structure. - - You are given: - - Pre-identified scope candidates (from deterministic manifest/indicator analysis) - - Orphan files that could not be assigned deterministically - - A directory tree of the repository - - SCOPE IDENTIFICATION FROM FILE PATHS: - Analyze orphan file paths to find the best matching scope: - 1. Extract common directory prefixes from orphan files to find candidate scopes - 2. Look for scope indicator patterns at ANY DEPTH in the path: - - svc/, services/, microservices/ -> service scope - - libs/, packages/, shared/, common/, core/ -> library scope - - apps/, web/, frontend/, mobile/ -> application scope - - scripts/, bin/, tools/, hack/, ci/, .github/, .gitlab/ -> script scope - 3. Examples of nested scope detection: - - File: mono/svc/my-service-v1/internal/handler.go - -> Scope: mono/svc/my-service-v1 ("svc/" indicates service) - - File: platform/packages/auth/src/index.ts - -> Scope: platform/packages/auth ("packages/" indicates library) - 4. Group files by longest common directory prefix containing a scope indicator - - SCOPE TYPE CLASSIFICATION: - These patterns can appear at ANY nesting depth: - - library: Shared code that others import - * Patterns: */libs/*, */packages/*, */shared/*, */common/*, */core/*, */sdk/* - - service: Isolated microservice with APIs - * Patterns: */services/*, */microservices/*, */svc/*, */cmd/* - - application: Standalone app or frontend - * Patterns: */apps/*, */web/*, */frontend/*, */mobile/* - - script: Build/deployment scripts, tooling, infrastructure - * Patterns: */scripts/*, */bin/*, */tools/*, */ci/*, */.github/*, */infra/* - - MONO-REPO AWARENESS: - - Scope indicator directories (packages/, services/, apps/, svc/) can appear at any level - - Prefer the deepest directory that forms a logical boundary - - Use repo_tree to verify directory structure exists +class ScopeRefinementSignature(dspy.Signature): + """Refine and finalize scope assignments for a pull request. + + You receive deterministic scope candidates (heuristic proposals) and unassigned files. + You have tools to explore the repository filesystem and search code. + + TOOLS AVAILABLE: + - list_directory: see directory contents + - get_tree: get subtree structure (use sparingly, targeted) + - read_file: read manifest files to understand package boundaries + - search_literal: find patterns across the codebase + - find_imports_of: understand dependencies between directories + + YOUR ROLE: + Produce the MINIMAL correct set of scopes. The deterministic heuristics provide + a starting point — validate, merge, or reclassify as needed. + + REFINEMENT OPERATIONS: + 1. MERGE: Combine candidates that share a deployment/release boundary + 2. RECLASSIFY: Change scope_type if the heuristic got it wrong + 3. ASSIGN: Place unassigned files into the best matching scope + 4. CREATE: New scope only when files clearly belong to an undiscovered boundary + 5. DROP: Remove candidates with no files and no structural value + + WHEN TO USE TOOLS: + - Use list_directory or get_tree to verify a directory boundary exists + - Use read_file on manifest files to check if two directories share a package + - Use find_imports_of to check if directories are coupled (merge signal) + - Do NOT explore exhaustively — only when a decision requires verification CRITICAL RULES: - 1. EVERY orphan file must be assigned to exactly ONE scope - 2. Don't create overlapping scopes (parent contains child) - 3. Prefer the most specific scope -- deepest directory that forms a logical boundary - 4. Use "." as scope ONLY when files are truly root-level with no nested structure - 5. Assign orphans to existing candidates when paths are compatible (file is under candidate subroot) - 6. Create new scopes only when orphan files clearly belong to an undiscovered boundary - - OUTPUT: Include ALL files (from candidates AND orphans) in the final scope assignments. - Group files by common directory prefix. Keep reasoning concise. + 1. Every changed file must be assigned to exactly ONE scope + 2. No overlapping scopes (parent contains child) + 3. Candidates marked "manifest=..." are backed by a real package manifest — they + represent a single deployable unit. Internal directories (tools/, agents/, lib/) + within a manifest scope typically belong to that scope. + 4. Prefer FEWER scopes. 1-3 scopes is typical for most PRs. + 5. When in doubt, MERGE into fewer scopes rather than split. + + OUTPUT: Final refined scope assignments with ALL changed files distributed. """ candidates: str = dspy.InputField( - desc="Pre-identified scope candidates with files already assigned (one per line)" + desc="Deterministic scope candidates with manifest info and file lists. Subject to refinement." ) orphan_files: list[str] = dspy.InputField( - desc="File paths that could not be assigned to any candidate scope" + desc="Changed files not assigned to any candidate (may be empty list)" ) - repo_tree: str = dspy.InputField(desc="Directory tree of repo (depth=6)") - mr_title: str = dspy.InputField(desc="PR title for context") - mr_description: str = dspy.InputField(desc="PR description for context") + mr_title: str = dspy.InputField(desc="PR title for intent context") + mr_description: str = dspy.InputField(desc="PR description for intent context") scopes: list[ScopeAssignment] = dspy.OutputField( - desc="Final scope assignments including all files from candidates and resolved orphans" + desc="Final refined scope assignments — all changed files must appear in exactly one scope" ) @@ -349,6 +334,31 @@ def __init__(self) -> None: self._cost_tracker = get_cost_tracker() self._settings = get_settings() + async def _create_tools(self, repo_path: Path) -> tuple[list[Any], list[Any]]: + """Create tools for the scope agent: filesystem + ripgrep. + + Args: + repo_path: Path to the repository root (not scope-restricted) + + Returns: + Tuple of (tools list, contexts list for cleanup) + """ + tools: list[Any] = [] + contexts: list[Any] = [] + tools_dir = Path(__file__).parent.parent.parent.parent / "tools" + repo_path_str = str(repo_path) + caller = "scope_resolver" + + tools.extend(await connect_mcp_server( + tools_dir / "storage" / "filesystem" / "server.py", + [repo_path_str], contexts, caller, + )) + tools.extend(await connect_mcp_server( + tools_dir / "parsers" / "ripgrep" / "server.py", + [repo_path_str], contexts, caller, + )) + return tools, contexts + async def _ensure_repo( self, mr: MergeRequest, repo_path: Path, is_local: bool ) -> None: @@ -433,8 +443,21 @@ def _resolve( reason=f"manifest {manifest_filename} at {subroot}/", ) - # Add scope-indicator-based scopes for uncovered paths + # Determine if root manifest is the sole manifest (single-package repo) + has_nested_manifests = any(subroot != "." for subroot in scopes) + root_suppresses = "." in scopes and not has_nested_manifests + + # Add scope-indicator-based scopes ONLY for files not covered by a manifest scope for file in changed_files: + # Check if file is covered by a non-root manifest + covered_by_nested = any( + subroot != "." and file.filename.startswith(subroot + "/") + for subroot in scopes + ) + # Root suppresses all indicators when it's the only manifest + if covered_by_nested or root_suppresses: + continue + indicator_type, indicator_path = self._find_scope_indicator(file.filename) if indicator_path and indicator_type and indicator_path not in scopes: scopes[indicator_path] = ScopeResult( @@ -442,7 +465,7 @@ def _resolve( subroot=indicator_path, scope_type=indicator_type, confidence=0.9, - reason="scope indicator in path", + reason="scope indicator in path (no parent manifest)", ) # Assign files to deepest matching scope @@ -637,132 +660,109 @@ def _find_scope_indicator(self, filepath: str) -> tuple[ScopeType | None, str]: return None, "" - def _build_repo_tree( - self, repo_path: Path, max_depth: int = 6, max_lines: int = 200 - ) -> str: - """Build a string representation of the repo tree. + def _format_candidate(self, s: ScopeResult) -> str: + """Format a scope candidate for the LLM. Args: - repo_path: Path to the repository root - max_depth: Maximum depth to traverse - max_lines: Maximum lines to return + s: Scope result to format Returns: - Tree string for LLM context + Formatted candidate string """ - lines: list[str] = [] - excluded_dirs = self._settings.excluded_directories - - for root, dirs, files in os.walk(repo_path): - rel_root = Path(root).relative_to(repo_path) - depth = len(rel_root.parts) if str(rel_root) != "." else 0 - - if depth > max_depth: - dirs[:] = [] - continue - - dirs[:] = [d for d in dirs if d not in excluded_dirs and not d.startswith(".")] - - indent = " " * depth - dir_name = Path(root).name if depth > 0 else "." - - manifest_markers = [] - for f in files: - if f in MANIFEST_FILES: - manifest_markers.append(f) - for pattern in MANIFEST_GLOBS: - if fnmatch.fnmatch(f, pattern): - manifest_markers.append(f) + manifest_info = "" + if s.package_manifest: + manifest_info = f", manifest={s.package_manifest.manifest_path}" + files = ", ".join(f.filename for f in s.changed_files) + return f"- {s.subroot} ({s.scope_type.value}{manifest_info}): files=[{files}]" - marker_str = f" ({', '.join(manifest_markers)})" if manifest_markers else "" - lines.append(f"{indent}{dir_name}/{marker_str}") - - return "\n".join(lines[:max_lines]) - - async def _resolve_orphans( + async def _refine_scopes( self, scopes: list[ScopeResult], orphans: list[ChangedFile], - repo_tree: str, mr: MergeRequest, + repo_path: Path, review_context: ReviewContext | None, run_id: str | None, ) -> list[ScopeResult]: - """Use LLM to resolve ambiguous scope assignments. + """Use ReAct agent to refine scope assignments from deterministic candidates. Args: scopes: Already-resolved scope results orphans: Orphan files that couldn't be assigned - repo_tree: Directory tree for context mr: The merge request + repo_path: Path to the repository root review_context: Optional review context with memory run_id: Pipeline run identifier Returns: - List of ScopeResult with LLM-resolved assignments + List of ScopeResult with agent-resolved assignments """ # Build candidates string from already-resolved scopes - candidates_str = "\n".join( - f"- {s.subroot} ({s.scope_type.value}): " - f"files=[{', '.join(f.filename for f in s.changed_files)}]" - for s in scopes - ) - - predictor = dspy.ChainOfThought(ScopeClassifierSignature) - mem: Hippocampus | None = None + candidates_str = "\n".join(self._format_candidate(s) for s in scopes) - async with SignatureContext("scope", self._cost_tracker): - if self._settings.get_memory_enabled("scope") and review_context: - question = ( - f"classify scopes of {review_context.pr_context.repo_slug}: " - f"pull request {review_context.pr_context.mr_number} " - f"{review_context.pr_context.mr_title}: {review_context.pr_context.summary}" - ) - mem = Hippocampus( - predictor, - budget=self._settings.get_memory_budget("scope"), - max_reflects=self._settings.get_memory_max_reflects("scope"), - question=question, - task_name="scope", - run_id=run_id, - initial_memory=review_context.memory if review_context else None, - ) - result = await mem.aforward( - candidates=candidates_str, - orphan_files=[f.filename for f in orphans], - repo_tree=repo_tree, - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", - ) - else: - result = await predictor.acall( - candidates=candidates_str, - orphan_files=[f.filename for f in orphans], - repo_tree=repo_tree, - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", - ) + max_iters = self._settings.get_max_iters("scope") + tools, contexts = await self._create_tools(repo_path) + try: + agent = dspy.ReAct( + signature=ScopeRefinementSignature, + tools=tools, + max_iters=max_iters, + ) + mem: Hippocampus | None = None + + async with SignatureContext("scope", self._cost_tracker): + if self._settings.get_memory_enabled("scope") and review_context: + question = ( + f"refine scopes of {review_context.pr_context.repo_slug}: " + f"PR #{review_context.pr_context.mr_number} " + f"{review_context.pr_context.mr_title}: " + f"{review_context.pr_context.summary}" + ) + mem = Hippocampus( + agent, + budget=self._settings.get_memory_budget("scope"), + max_reflects=self._settings.get_memory_max_reflects("scope"), + question=question, + task_name="scope", + run_id=run_id, + initial_memory=review_context.memory if review_context else None, + ) + result = await mem.aforward( + candidates=candidates_str, + orphan_files=[f.filename for f in orphans], + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + ) + else: + result = await agent.acall( + candidates=candidates_str, + orphan_files=[f.filename for f in orphans], + mr_title=mr.title or "No title", + mr_description=mr.body or "No description", + ) - # Build file map from all known files - all_files = {f.filename: f for s in scopes for f in s.changed_files} - all_files.update({f.filename: f for f in orphans}) + # Build file map from all known files + all_files = {f.filename: f for s in scopes for f in s.changed_files} + all_files.update({f.filename: f for f in orphans}) - final_scopes = self._convert_assignments(result.scopes, all_files, mr.repo_slug) + final_scopes = self._convert_assignments(result.scopes, all_files, mr.repo_slug) - # Persist episode at deepest common folder when LLM fallback was used and memory is enabled - if mem is not None: - common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) - scope_desc = "\n".join( - f"- {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" - for s in final_scopes - ) - await mem.aend_episode( - get_memory_store(self._settings), - common_dir, - artifacts={"scopes": scope_desc}, - ) + # Persist episode at deepest common folder when memory is enabled + if mem is not None: + common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) + scope_desc = "\n".join( + f"- {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" + for s in final_scopes + ) + await mem.aend_episode( + get_memory_store(self._settings), + common_dir, + artifacts={"scopes": scope_desc}, + ) - return final_scopes + return final_scopes + finally: + await cleanup_mcp_contexts(contexts) def _convert_assignments( self, @@ -844,13 +844,15 @@ async def aforward( try: await self._ensure_repo(mr, repo_path, is_local) scopes, orphans = self._resolve(repo_path, reviewable_files, repo) - - if not orphans: - return scopes, review_context.memory if review_context else None - - # LLM fallback for orphans - repo_tree = self._build_repo_tree(repo_path) - scopes = await self._resolve_orphans(scopes, orphans, repo_tree, mr, review_context, run_id) + scopes = await self._refine_scopes( + scopes, orphans, mr, repo_path, review_context, run_id + ) + # Log final scopes for visibility + scope_summary = "\n".join( + f" - {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" + for s in scopes + ) + logger.info("Resolved %d scope(s) for %s:\n%s", len(scopes), mr.repo_slug, scope_summary) return scopes, review_context.memory if review_context else None except Exception as e: diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index fa578ac..8ae2b06 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -208,5 +208,89 @@ def test_strong_manifest_confidence(self): assert scopes[0].confidence == 0.9 + def test_internal_dir_not_scope_indicator(self): + """internal/ is not a scope indicator for sparse paths.""" + changed_files = ["backend/internal/cache/redis.go"] + paths = derive_sparse_paths(changed_files) + # Should use depth-2 fallback, not scope indicator + assert "backend/internal/" in paths # depth-2 prefix + + def test_indicator_suppressed_under_manifest(self): + """Indicator scopes not created when parent manifest covers file.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "services" / "api").mkdir(parents=True) + (repo_path / "services" / "api" / "go.mod").touch() + + changed_files = [ + ChangedFile(filename="services/api/cmd/main.go", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + # Should be 1 scope (from manifest), not 2 (manifest + cmd/ indicator) + assert len(scopes) == 1 + assert scopes[0].subroot == "services/api" + assert len(orphans) == 0 + + def test_root_manifest_suppresses_indicators_when_sole_manifest(self): + """Root package.json as sole manifest suppresses indicator scopes.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "package.json").touch() + (repo_path / "scripts" / "deploy").mkdir(parents=True) + + changed_files = [ + ChangedFile(filename="scripts/deploy/prod.sh", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + # scripts/ indicator is suppressed when root is the only manifest (single-package repo) + scope_subroots = [s.subroot for s in scopes] + assert len(scopes) == 1 + assert scopes[0].subroot == "." + + def test_root_manifest_suppresses_when_sole_manifest(self): + """Single-package repo: root manifest suppresses all indicators.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "pyproject.toml").touch() + (repo_path / "src" / "pkg" / "tools" / "git").mkdir(parents=True) + + changed_files = [ + ChangedFile(filename="src/pkg/tools/git/client.py", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + assert len(scopes) == 1 + assert scopes[0].subroot == "." + assert len(orphans) == 0 + + def test_root_does_not_suppress_when_nested_manifests_exist(self): + """Monorepo: root is container, indicators still fire for uncovered files.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / "package.json").touch() + (repo_path / "packages" / "auth").mkdir(parents=True) + (repo_path / "packages" / "auth" / "package.json").touch() + (repo_path / "scripts" / "deploy").mkdir(parents=True) + + changed_files = [ + ChangedFile(filename="scripts/deploy/prod.sh", status=FileStatus.MODIFIED), + ] + + resolver = ScopeResolver() + scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") + + # scripts/deploy indicator should fire — root has nested manifests + scope_subroots = [s.subroot for s in scopes] + assert "scripts/deploy" in scope_subroots + + if __name__ == "__main__": pytest.main([__file__, "-v"]) From 6d1c9ef11c7387f64d56f686ab7f4a649a2dc8da Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 09:55:21 +0200 Subject: [PATCH 49/79] wip --- .env.example | 2 +- codespy.yaml | 2 +- .../agents/reviewer/modules/scope_resolver.py | 297 ++++++++++++------ src/codespy/config.py | 2 +- 4 files changed, 207 insertions(+), 96 deletions(-) diff --git a/.env.example b/.env.example index 78fa7f6..3b92c30 100644 --- a/.env.example +++ b/.env.example @@ -124,7 +124,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Falls back to DEFAULT_MODEL if not set # EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 -# DEFAULT_MAX_ITERS=3 +# DEFAULT_MAX_ITERS=10 # Provider reasoning budget: minimal | low | medium | high # DEFAULT_REASONING_EFFORT=medium # Must be 1 while reasoning is enabled (providers reject other values) diff --git a/codespy.yaml b/codespy.yaml index 451ca34..a94bc16 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -172,7 +172,7 @@ memory: # These apply to all signatures and reflection modules unless overridden default_model: anthropic/claude-opus-4-6 # DEFAULT_MODEL extraction_model: null # EXTRACTION_MODEL (falls back to default_model) -default_max_iters: 5 # DEFAULT_MAX_ITERS +default_max_iters: 10 # DEFAULT_MAX_ITERS default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) default_temperature: 1 # DEFAULT_TEMPERATURE (must be 1 while reasoning is enabled) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 81c0cce..7513676 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -13,7 +13,7 @@ import logging import os from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import Any import dspy # type: ignore[import-untyped] from pydantic import BaseModel, Field @@ -205,35 +205,16 @@ def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: ) -class ScopeAssignment(BaseModel): - """LLM-friendly scope assignment with string file paths. +class ScopeBoundary(BaseModel): + """LLM output: a scope boundary decision (no file listing).""" - Used for LLM output in the fallback path. - """ - - subroot: str = Field(description="Path relative to repo root (e.g., packages/auth)") - scope_type: ScopeType = Field(description="Type of scope (library, service, etc.)") - has_changes: bool = Field( - default=False, description="Whether this scope has changed files from PR" - ) - is_dependency: bool = Field( - default=False, description="Whether this scope depends on a changed scope" - ) - confidence: float = Field( - default=0.8, ge=0.0, le=1.0, description="Confidence score for scope identification" - ) - language: str | None = Field(default=None, description="Primary language detected") - package_manifest: PackageManifest | None = Field( - default=None, description="Package manifest info if present" - ) - changed_files: list[str] = Field( - default_factory=list, description="Changed file paths belonging to this scope" - ) - reason: str = Field(description="Explanation for why this scope was identified") + subroot: str = Field(description="Path relative to repo root (e.g., 'packages/auth' or '.' for root)") + scope_type: ScopeType = Field(description="Type of scope") + reason: str = Field(description="Brief explanation for this boundary") class ScopeRefinementSignature(dspy.Signature): - """Refine and finalize scope assignments for a pull request. + """Refine and finalize scope boundaries for a pull request. You receive deterministic scope candidates (heuristic proposals) and unassigned files. You have tools to explore the repository filesystem and search code. @@ -246,15 +227,17 @@ class ScopeRefinementSignature(dspy.Signature): - find_imports_of: understand dependencies between directories YOUR ROLE: - Produce the MINIMAL correct set of scopes. The deterministic heuristics provide + Produce the MINIMAL correct set of scope boundaries. The deterministic heuristics provide a starting point — validate, merge, or reclassify as needed. + Files are assigned automatically to the deepest matching boundary by path prefix. + You only need to decide WHERE boundaries are, not which files go where. + REFINEMENT OPERATIONS: 1. MERGE: Combine candidates that share a deployment/release boundary 2. RECLASSIFY: Change scope_type if the heuristic got it wrong - 3. ASSIGN: Place unassigned files into the best matching scope - 4. CREATE: New scope only when files clearly belong to an undiscovered boundary - 5. DROP: Remove candidates with no files and no structural value + 3. CREATE: New boundary only for clearly separate units (especially orphans) + 4. DROP: Remove candidates with no structural value WHEN TO USE TOOLS: - Use list_directory or get_tree to verify a directory boundary exists @@ -263,15 +246,15 @@ class ScopeRefinementSignature(dspy.Signature): - Do NOT explore exhaustively — only when a decision requires verification CRITICAL RULES: - 1. Every changed file must be assigned to exactly ONE scope + 1. Output only scope boundaries (subroots). Files are assigned automatically by path + prefix to the deepest matching scope. 2. No overlapping scopes (parent contains child) - 3. Candidates marked "manifest=..." are backed by a real package manifest — they - represent a single deployable unit. Internal directories (tools/, agents/, lib/) - within a manifest scope typically belong to that scope. + 3. Manifest-backed candidates (marked 'manifest=...') are authoritative. Keep them + unless merging multiple manifests into one. 4. Prefer FEWER scopes. 1-3 scopes is typical for most PRs. 5. When in doubt, MERGE into fewer scopes rather than split. - OUTPUT: Final refined scope assignments with ALL changed files distributed. + OUTPUT: Final refined scope boundaries. Files are assigned automatically. """ candidates: str = dspy.InputField( @@ -283,8 +266,8 @@ class ScopeRefinementSignature(dspy.Signature): mr_title: str = dspy.InputField(desc="PR title for intent context") mr_description: str = dspy.InputField(desc="PR description for intent context") - scopes: list[ScopeAssignment] = dspy.OutputField( - desc="Final refined scope assignments — all changed files must appear in exactly one scope" + scopes: list[ScopeBoundary] = dspy.OutputField( + desc="Scope boundaries (subroots). Files are assigned automatically — do NOT list files." ) @@ -322,6 +305,14 @@ def derive_sparse_paths(changed_files: list[str]) -> list[str]: paths = sorted(scope_roots) paths.append("/*") + # Explicitly add root manifest files to ensure they are checked out + # in sparse/treeless clones (/* pattern doesn't always work reliably) + for manifest in MANIFEST_FILES: + paths.append(manifest) + for manifest_pattern in MANIFEST_GLOBS: + # For glob patterns like *.csproj, we need to add the pattern itself + paths.append(manifest_pattern) + return paths @@ -374,7 +365,20 @@ async def _ensure_repo( return if repo_path.exists() and (repo_path / ".git").exists(): - logger.debug("Repo already cloned at %s", repo_path) + from git import Repo + logger.debug("Updating existing clone at %s", repo_path) + changed_file_paths = [f.filename for f in mr.changed_files] + sparse_paths = derive_sparse_paths(changed_file_paths) + # Update sparse-checkout config + sparse_file = repo_path / ".git" / "info" / "sparse-checkout" + sparse_file.parent.mkdir(parents=True, exist_ok=True) + sparse_file.write_text("\n".join(sparse_paths) + "\n") + # Fetch and checkout correct ref + repo = Repo(repo_path) + repo.git.fetch("origin", mr.head_sha, "--depth", "1") + repo.git.checkout(mr.head_sha) + # Ensure manifests at root + parent dirs + await self._ensure_manifests(repo_path, changed_file_paths) return changed_file_paths = [f.filename for f in mr.changed_files] @@ -403,6 +407,55 @@ async def _ensure_repo( ) logger.info("Clone complete: %s", repo_path) + # Ensure manifest files are present at root and parent directories + await self._ensure_manifests(repo_path, changed_file_paths) + + async def _ensure_manifests(self, repo_path: Path, changed_files: list[str]) -> None: + """Ensure manifest files at root and parent directories are checked out. + + Sparse/treeless clones may not materialize manifests at ancestor directories. + This explicitly checks out known manifest files at: + - Repository root + - Every ancestor directory of every changed file path + + Args: + repo_path: Path to the repository root + changed_files: List of changed file paths + """ + from git import Repo + + # Collect all ancestor directories of changed files + parent_dirs: set[str] = set() + for filepath in changed_files: + parts = filepath.split("/") + for depth in range(1, len(parts)): # skip filename, collect dirs + parent_dirs.add("/".join(parts[:depth])) + + # Build list of manifest paths to check + manifest_paths: list[str] = [] + + # Root manifests + for manifest in MANIFEST_FILES: + manifest_paths.append(manifest) + + # Parent manifests + for parent in parent_dirs: + for manifest in MANIFEST_FILES: + manifest_paths.append(f"{parent}/{manifest}") + + # Checkout missing manifests + try: + repo = Repo(repo_path) + for path in manifest_paths: + if not (repo_path / path).exists(): + try: + repo.git.checkout("HEAD", "--", path) + logger.debug("Checked out manifest: %s", path) + except Exception: + pass # File doesn't exist in repo — expected + except Exception as e: + logger.warning("Failed to ensure manifests: %s", e) + def _resolve( self, repo_path: Path, changed_files: list[ChangedFile], repo: str ) -> tuple[list[ScopeResult], list[ChangedFile]]: @@ -418,6 +471,12 @@ def _resolve( """ excluded_dirs = self._settings.excluded_directories manifests = self._discover_manifests(repo_path, excluded_dirs) + logger.info( + "Manifest discovery at %s found %d manifest(s): %s", + repo_path, + len(manifests), + {str(k): v[1] for k, v in manifests.items()}, + ) # Build ScopeResult per manifest scopes: dict[str, ScopeResult] = {} @@ -443,19 +502,15 @@ def _resolve( reason=f"manifest {manifest_filename} at {subroot}/", ) - # Determine if root manifest is the sole manifest (single-package repo) has_nested_manifests = any(subroot != "." for subroot in scopes) - root_suppresses = "." in scopes and not has_nested_manifests + root_is_sole_manifest = "." in scopes and not has_nested_manifests - # Add scope-indicator-based scopes ONLY for files not covered by a manifest scope for file in changed_files: - # Check if file is covered by a non-root manifest covered_by_nested = any( subroot != "." and file.filename.startswith(subroot + "/") for subroot in scopes ) - # Root suppresses all indicators when it's the only manifest - if covered_by_nested or root_suppresses: + if covered_by_nested or root_is_sole_manifest: continue indicator_type, indicator_path = self._find_scope_indicator(file.filename) @@ -487,6 +542,7 @@ def _discover_manifests( Returns: Dict mapping manifest directory -> (package manager, filename) """ + logger.debug("Walking %s for manifests (excluded: %s)", repo_path, excluded_dirs) manifests: dict[Path, tuple[str, str]] = {} excluded_set = set(excluded_dirs) @@ -600,9 +656,7 @@ def _dependencies_changed( manifest_path = str(manifest_dir / manifest_filename) if manifest_dir != Path(".") else manifest_filename if manifest_path in changed_paths: return True - if lock_file and str(lock_file) in changed_paths: - return True - return False + return bool(lock_file and str(lock_file) in changed_paths) def _assign_files( self, scopes: list[ScopeResult], changed_files: list[ChangedFile] @@ -675,6 +729,87 @@ def _format_candidate(self, s: ScopeResult) -> str: files = ", ".join(f.filename for f in s.changed_files) return f"- {s.subroot} ({s.scope_type.value}{manifest_info}): files=[{files}]" + def _apply_boundaries( + self, + boundaries: list[ScopeBoundary], + all_changed_files: list[ChangedFile], + deterministic_scopes: list[ScopeResult], + repo: str, + ) -> list[ScopeResult]: + """Apply LLM boundaries + manifest guardrail + deterministic file assignment. + + Args: + boundaries: Scope boundaries from LLM + all_changed_files: All changed files to assign + deterministic_scopes: Original deterministic scopes (for manifest info) + repo: Repo identifier + + Returns: + List of ScopeResult with files assigned + """ + # Index manifest-backed scopes from deterministic layer + manifest_scopes = { + s.subroot: s for s in deterministic_scopes if s.package_manifest + } + + # Build final boundary set + final_boundaries: dict[str, ScopeResult] = {} + + # 1. Always include manifest-backed scopes (immutable baseline) + for subroot, det_scope in manifest_scopes.items(): + final_boundaries[subroot] = ScopeResult( + repo=repo, + subroot=subroot, + scope_type=det_scope.scope_type, + confidence=0.9, + package_manifest=det_scope.package_manifest, + reason=det_scope.reason, + ) + + # 2. Add LLM boundaries — but discard if child of a manifest scope + for boundary in boundaries: + # Check if this boundary is inside a manifest-backed scope + inside_manifest = any( + boundary.subroot.startswith(ms + "/") or ms == "." + for ms in manifest_scopes + if ms != boundary.subroot + ) + if inside_manifest: + logger.debug( + "Discarding LLM boundary '%s' — inside manifest scope", boundary.subroot + ) + continue + + # LLM can override scope_type of manifest scopes + if boundary.subroot in final_boundaries: + final_boundaries[boundary.subroot].scope_type = boundary.scope_type + else: + final_boundaries[boundary.subroot] = ScopeResult( + repo=repo, + subroot=boundary.subroot, + scope_type=boundary.scope_type, + confidence=0.8, + reason=boundary.reason, + ) + + # 3. Deterministic file assignment to deepest matching boundary + orphans = self._assign_files(list(final_boundaries.values()), all_changed_files) + + # 4. Stragglers → root scope + if orphans: + if "." in final_boundaries: + for f in orphans: + final_boundaries["."].changed_files.append(f) + final_boundaries["."].has_changes = True + else: + final_boundaries["."] = ScopeResult( + repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, + has_changes=True, confidence=0.7, changed_files=orphans, + reason="catch-all for unmatched files", + ) + + return [s for s in final_boundaries.values() if s.changed_files] + async def _refine_scopes( self, scopes: list[ScopeResult], @@ -741,11 +876,13 @@ async def _refine_scopes( mr_description=mr.body or "No description", ) - # Build file map from all known files - all_files = {f.filename: f for s in scopes for f in s.changed_files} - all_files.update({f.filename: f for f in orphans}) + # Collect all changed files (from scopes + orphans) + all_files = [f for s in scopes for f in s.changed_files] + orphans - final_scopes = self._convert_assignments(result.scopes, all_files, mr.repo_slug) + # Apply LLM boundaries with manifest guardrail and deterministic file assignment + final_scopes = self._apply_boundaries( + result.scopes, all_files, scopes, mr.repo_slug + ) # Persist episode at deepest common folder when memory is enabled if mem is not None: @@ -764,48 +901,6 @@ async def _refine_scopes( finally: await cleanup_mcp_contexts(contexts) - def _convert_assignments( - self, - assignments: list[ScopeAssignment], - changed_files_map: dict[str, ChangedFile], - repo: str, - ) -> list[ScopeResult]: - """Convert LLM scope assignments to ScopeResults. - - Args: - assignments: Scope assignments from LLM - changed_files_map: Map from filename to ChangedFile - repo: Repo identifier - - Returns: - List of ScopeResult - """ - results: list[ScopeResult] = [] - for assignment in assignments: - changed_files: list[ChangedFile] = [] - for filepath in assignment.changed_files: - if filepath in changed_files_map: - changed_files.append(changed_files_map[filepath]) - else: - logger.warning( - "File '%s' from scope assignment not found in PR", filepath - ) - results.append( - ScopeResult( - repo=repo, - subroot=assignment.subroot, - scope_type=assignment.scope_type, - has_changes=assignment.has_changes, - is_dependency=assignment.is_dependency, - confidence=assignment.confidence, - language=assignment.language, - package_manifest=assignment.package_manifest, - changed_files=changed_files, - reason=assignment.reason, - ) - ) - return results - async def aforward( self, mr: MergeRequest, @@ -844,6 +939,22 @@ async def aforward( try: await self._ensure_repo(mr, repo_path, is_local) scopes, orphans = self._resolve(repo_path, reviewable_files, repo) + + # Log deterministic scopes before LLM refinement + if scopes: + det_summary = "\n".join( + f" - {s.subroot} ({s.scope_type.value})" + f"{f', manifest={s.package_manifest.manifest_path}' if s.package_manifest else ''}" + f": {len(s.changed_files)} files" + for s in scopes + ) + logger.info( + "Deterministic scope identification found %d scope(s):\n%s", + len(scopes), det_summary + ) + if orphans: + logger.info("Deterministic identification produced %d orphan(s)", len(orphans)) + scopes = await self._refine_scopes( scopes, orphans, mr, repo_path, review_context, run_id ) diff --git a/src/codespy/config.py b/src/codespy/config.py index a20c963..deee891 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -124,7 +124,7 @@ class Settings(BaseSettings): # Top-level defaults (also available via env vars DEFAULT_MODEL, etc.) default_model: str = "anthropic/claude-opus-4-6" extraction_model: str | None = None # TwoStepAdapter extraction (falls back to default_model) - default_max_iters: int = 5 + default_max_iters: int = 10 # Provider reasoning budget; LiteLLM maps this to each provider's native parameter. default_reasoning_effort: ReasoningEffort = "medium" # Providers require temperature=1 when reasoning is enabled. From 0babecbe07b748242975fd480976980680e3c94e Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 10:06:30 +0200 Subject: [PATCH 50/79] wip --- .../agents/reviewer/modules/scope_resolver.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 7513676..1d9aa86 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -313,6 +313,19 @@ def derive_sparse_paths(changed_files: list[str]) -> list[str]: # For glob patterns like *.csproj, we need to add the pattern itself paths.append(manifest_pattern) + # Agent config directories — project instructions for ReAct agents + paths.extend([ + ".claude/", + ".kilo/", + ".agent/", + ".ai/", + ".cursor/", + ".codex/", + "AGENTS.md", + "CLAUDE.md", + "SKILL.md", + ]) + return paths From 0f801657f1a9cdeabeb55c17ea3b48911174f9ee Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 18:27:50 +0200 Subject: [PATCH 51/79] wip --- src/codespy/agents/reviewer/models.py | 6 +- .../agents/reviewer/modules/doc_reviewer.py | 3 + .../agents/reviewer/modules/helpers.py | 2 +- .../agents/reviewer/modules/scope_resolver.py | 77 ++++++++++++++++--- src/codespy/agents/reviewer/reviewer.py | 45 ++++++++++- src/codespy/config_memory.py | 18 ++++- tests/test_scope_resolver.py | 11 ++- 7 files changed, 140 insertions(+), 22 deletions(-) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 2c8fba8..fd6de06 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -93,9 +93,6 @@ class ScopeResult(BaseModel): is_dependency: bool = Field( default=False, description="Whether this scope depends on a changed scope" ) - confidence: float = Field( - default=0.8, ge=0.0, le=1.0, description="Confidence score for scope identification" - ) language: str | None = Field(default=None, description="Primary language detected") package_manifest: PackageManifest | None = Field( default=None, description="Package manifest info if present" @@ -104,6 +101,9 @@ class ScopeResult(BaseModel): default_factory=list, description="Changed files belonging to this scope" ) reason: str = Field(description="Explanation for why this scope was identified") + skills: str | None = Field( + default=None, description="Project/scope instructions inherited from ancestor directories" + ) model_config = {"arbitrary_types_allowed": True} diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 5908f58..f7455e5 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -69,6 +69,7 @@ class DocReviewSignature(dspy.Signature): OUTPUT RULES: - Set category to "documentation" + - filename: the documentation file that needs updating (use path from === path === headers) - description: ≤25 words, imperative tone ("Update X section", "Add Y to README") - Empty list if documentation is up to date. No approval text ("LGTM", "looks good") - No polite or conversational language @@ -88,6 +89,8 @@ class DocReviewSignature(dspy.Signature): issues: list[Issue] = dspy.OutputField( desc="Documentation issues. Category must be 'documentation'. " + "Each issue MUST include 'filename' set to the documentation file that needs " + "updating (from the === filename === headers in the documentation input). " "Titles <10 words. Descriptions ≤25 words, imperative. Empty list if none." ) diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index 9220c7e..d1fa5d3 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -156,11 +156,11 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: scope_type=scope.scope_type, has_changes=scope.has_changes, is_dependency=scope.is_dependency, - confidence=scope.confidence, language=scope.language, package_manifest=manifest, changed_files=relative_files, reason=scope.reason, + skills=scope.skills, ) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 1d9aa86..d152319 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -34,6 +34,52 @@ logger = logging.getLogger(__name__) +# Files to read at each directory level +SKILL_FILES: list[str] = ["AGENTS.md", "CLAUDE.md", "SKILL.md"] + +# Subdirectories to scan for .md files (limited depth) +SKILL_DIRS: list[str] = [".kilo/agent", ".claude", ".agent", ".ai", ".cursor", ".codex"] + + +def collect_skills(repo_path: Path, subroot: str) -> str | None: + """Collect hierarchical skills from root down to scope subroot. + + Reads instruction files at each ancestor directory level. + Returns concatenated content (root-first) or None if nothing found. + """ + # Build path hierarchy: [".", "packages", "packages/auth"] + levels: list[str] = ["."] + if subroot and subroot != ".": + parts = subroot.split("/") + for i in range(1, len(parts) + 1): + levels.append("/".join(parts[:i])) + + sections: list[str] = [] + + for level in levels: + level_path = repo_path if level == "." else repo_path / level + + # Read standalone skill files + for filename in SKILL_FILES: + filepath = level_path / filename + if filepath.is_file(): + content = filepath.read_text(errors="ignore").strip() + if content: + header = filename if level == "." else f"{level}/{filename}" + sections.append(f"=== {header} ===\n{content}") + + # Read .md files from skill directories + for skill_dir in SKILL_DIRS: + dir_path = level_path / skill_dir + if dir_path.is_dir(): + for md_file in sorted(dir_path.glob("*.md")): + content = md_file.read_text(errors="ignore").strip() + if content: + rel = f"{level}/{skill_dir}/{md_file.name}" if level != "." else f"{skill_dir}/{md_file.name}" + sections.append(f"=== {rel} ===\n{content}") + + return "\n\n".join(sections) if sections else None + def _deepest_common_folder(scopes: list[ScopeResult], repo_slug: str) -> str: """Compute the deepest common ancestor directory across all scope subroots. @@ -216,7 +262,8 @@ class ScopeBoundary(BaseModel): class ScopeRefinementSignature(dspy.Signature): """Refine and finalize scope boundaries for a pull request. - You receive deterministic scope candidates (heuristic proposals) and unassigned files. + You receive deterministic scope candidates (heuristic proposals), unassigned files, + and project instructions that describe the repository structure and conventions. You have tools to explore the repository filesystem and search code. TOOLS AVAILABLE: @@ -265,6 +312,9 @@ class ScopeRefinementSignature(dspy.Signature): ) mr_title: str = dspy.InputField(desc="PR title for intent context") mr_description: str = dspy.InputField(desc="PR description for intent context") + project_instructions: str = dspy.InputField( + desc="Project coding guidelines and structure context from config files (AGENTS.md, .kilo/, etc.). May be empty." + ) scopes: list[ScopeBoundary] = dspy.OutputField( desc="Scope boundaries (subroots). Files are assigned automatically — do NOT list files." @@ -505,7 +555,6 @@ def _resolve( repo=repo, subroot=subroot, scope_type=scope_type, - confidence=0.9, package_manifest=PackageManifest( manifest_path=manifest_path, lock_file_path=str(lock_file) if lock_file else None, @@ -532,7 +581,6 @@ def _resolve( repo=repo, subroot=indicator_path, scope_type=indicator_type, - confidence=0.9, reason="scope indicator in path (no parent manifest)", ) @@ -774,7 +822,6 @@ def _apply_boundaries( repo=repo, subroot=subroot, scope_type=det_scope.scope_type, - confidence=0.9, package_manifest=det_scope.package_manifest, reason=det_scope.reason, ) @@ -801,7 +848,6 @@ def _apply_boundaries( repo=repo, subroot=boundary.subroot, scope_type=boundary.scope_type, - confidence=0.8, reason=boundary.reason, ) @@ -817,7 +863,7 @@ def _apply_boundaries( else: final_boundaries["."] = ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, - has_changes=True, confidence=0.7, changed_files=orphans, + has_changes=True, changed_files=orphans, reason="catch-all for unmatched files", ) @@ -848,6 +894,9 @@ async def _refine_scopes( # Build candidates string from already-resolved scopes candidates_str = "\n".join(self._format_candidate(s) for s in scopes) + # Read root-level project instructions for the scope agent + project_instructions = collect_skills(repo_path, ".") or "" + max_iters = self._settings.get_max_iters("scope") tools, contexts = await self._create_tools(repo_path) try: @@ -880,6 +929,7 @@ async def _refine_scopes( orphan_files=[f.filename for f in orphans], mr_title=mr.title or "No title", mr_description=mr.body or "No description", + project_instructions=project_instructions, ) else: result = await agent.acall( @@ -887,6 +937,7 @@ async def _refine_scopes( orphan_files=[f.filename for f in orphans], mr_title=mr.title or "No title", mr_description=mr.body or "No description", + project_instructions=project_instructions, ) # Collect all changed files (from scopes + orphans) @@ -897,6 +948,10 @@ async def _refine_scopes( result.scopes, all_files, scopes, mr.repo_slug ) + # Attach hierarchical skills to each produced scope + for scope in final_scopes: + scope.skills = collect_skills(repo_path, scope.subroot) + # Persist episode at deepest common folder when memory is enabled if mem is not None: common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) @@ -943,11 +998,13 @@ async def aforward( repo = mr.repo_slug if not self._settings.is_signature_enabled("scope"): - return [ScopeResult( + fallback = ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, - has_changes=True, confidence=0.5, changed_files=reviewable_files, + has_changes=True, changed_files=reviewable_files, reason="Scope identification disabled", - )], review_context.memory if review_context else None + ) + fallback.skills = collect_skills(repo_path, ".") + return [fallback], review_context.memory if review_context else None try: await self._ensure_repo(mr, repo_path, is_local) @@ -983,7 +1040,7 @@ async def aforward( logger.error("Scope resolution failed: %s", e, exc_info=True) return [ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, - has_changes=True, confidence=0.5, changed_files=reviewable_files, + has_changes=True, changed_files=reviewable_files, reason=f"Fallback due to error: {e}", )], review_context.memory if review_context else None diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 333d73e..874e54c 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -33,6 +33,7 @@ SupplyChainAuditor, ) from codespy.agents.reviewer.modules.helpers import build_patches +from codespy.agents.reviewer.modules.scope_resolver import MANIFEST_FILES, MANIFEST_GLOBS logger = logging.getLogger(__name__) @@ -200,6 +201,9 @@ def forward(self, config: ReviewConfig) -> ReviewResult: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: logger.info(f" Dependencies changed: Yes") + # Expand sparse checkout to cover full scope subtrees + if not is_local: + self._expand_sparse_for_scopes(scopes, repo_path) # Compact patches: expand context to function bodies for better review context logger.info("Compacting patches to function boundaries...") changed_file_paths = [f.filename for f in mr.changed_files] @@ -303,4 +307,43 @@ def _collect_signature_stats(self) -> list[SignatureStatsResult]: duration_seconds=stats.duration_seconds, )) - return stats_list \ No newline at end of file + return stats_list + + def _expand_sparse_for_scopes( + self, scopes: list, repo_path: Path + ) -> None: + """Expand sparse checkout to cover full subtree of each identified scope. + + Called after scope identification, before compact_patches and review modules, + to ensure read_file and patch compaction have full scope context available. + """ + from git import Repo + + git_dir = repo_path / ".git" + if not git_dir.exists(): + return + + # Build scope-aware sparse paths + sparse_paths: set[str] = set() + for scope in scopes: + if scope.subroot == ".": + # Root scope — need everything; disable sparse checkout effectively + sparse_paths.add("/*") + sparse_paths.add("*/") + break + else: + sparse_paths.add(scope.subroot.rstrip("/") + "/") + + # Always include root-level files and manifests + sparse_paths.add("/*") + for manifest in MANIFEST_FILES: + sparse_paths.add(manifest) + for pattern in MANIFEST_GLOBS: + sparse_paths.add(pattern) + + sparse_file = git_dir / "info" / "sparse-checkout" + sparse_file.write_text("\n".join(sorted(sparse_paths)) + "\n") + + # Re-checkout to materialize newly included paths + repo = Repo(repo_path) + repo.git.checkout() \ No newline at end of file diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index a989839..6f674e2 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -2,10 +2,13 @@ from __future__ import annotations +import logging import os from typing import TYPE_CHECKING, Any, Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator + +logger = logging.getLogger(__name__) from codespy.config_dspy import ReasoningEffort from codespy.tools.storage.base import Storage @@ -55,6 +58,19 @@ class LLMSettings(BaseModel): # ``new_lm`` clamps this to the model's real output ceiling before use. max_tokens: int + @model_validator(mode="after") + def _enforce_temperature_with_reasoning(self) -> "LLMSettings": + """Providers require temperature=1 when reasoning is enabled.""" + if self.reasoning_effort is not None and self.temperature != 1.0: + logger.warning( + "temperature=%.2f is incompatible with reasoning_effort=%r; " + "forcing temperature=1.0", + self.temperature, + self.reasoning_effort, + ) + self.temperature = 1.0 + return self + class MemoryConfig(BaseModel): diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index 8ae2b06..a8184b7 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -187,11 +187,11 @@ def test_scope_type_classification(self): assert by_subroot["apps/web"].scope_type == ScopeType.APPLICATION -class TestConfidenceScoring: - """Test confidence scoring based on manifest types.""" +class TestManifestScoping: + """Test scope detection based on manifests.""" - def test_strong_manifest_confidence(self): - """Test strong manifests get 0.9 confidence.""" + def test_manifest_creates_scope(self): + """Test manifests create proper scopes.""" with tempfile.TemporaryDirectory() as tmpdir: repo_path = Path(tmpdir) (repo_path / "go.mod").touch() @@ -203,9 +203,8 @@ def test_strong_manifest_confidence(self): resolver = ScopeResolver() scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") - # Candidate should exist with manifest and 0.9 confidence + # Candidate should exist with manifest assert scopes[0].package_manifest is not None - assert scopes[0].confidence == 0.9 def test_internal_dir_not_scope_indicator(self): From 330c154594bb209f6b14098d6c6f5756b5cc4e49 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 18:53:52 +0200 Subject: [PATCH 52/79] wip --- .../agents/memory/hippocampus/hippocampus.py | 5 +- .../agents/reviewer/modules/scope_resolver.py | 2 +- src/codespy/tools/git/patch_utils.py | 107 ++++++++---------- src/codespy/tools/storage/s3/client.py | 25 ++-- tests/test_s3_client.py | 58 ++++++++++ tests/test_scope_resolver.py | 26 +++++ 6 files changed, 148 insertions(+), 75 deletions(-) create mode 100644 tests/test_s3_client.py diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index fd1d10a..91e2236 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -185,7 +185,7 @@ class for per-field guidance. Resolve one from configuration with # and to detect "everything was reflected online" so end_episode() still # persists a snapshot even when nothing remains to consolidate. self._reflected_count: int = 0 - # Question derived from the first buffered call; used as Distiller input. + # Question derived from the latest buffered call; used as Distiller consolidation input. self._episode_question: str | None = None # Identity of the wrapped module/signature for Episode metadata. An explicit @@ -238,8 +238,7 @@ def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None: """ traj = format_trajectory(pred, self.budget.max_trajectory_tokens) self._episode_trajectories.append(traj) - if self._episode_question is None: - self._episode_question = self._make_question(kwargs) + self._episode_question = self._make_question(kwargs) # Online reflection: None = no limit (always); N = for the first N calls. if (self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects): diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index d152319..8ad5a96 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -732,7 +732,7 @@ def _assign_files( List of orphan files that couldn't be assigned """ # Sort by depth (deepest first) for greedy assignment - sorted_scopes = sorted(scopes, key=lambda s: s.subroot.count("/"), reverse=True) + sorted_scopes = sorted(scopes, key=lambda s: (-s.subroot.count("/"), s.subroot)) orphans: list[ChangedFile] = [] for file in changed_files: diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index aec65bd..b94f24e 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -361,6 +361,49 @@ def _merge_hunks(expanded_hunks: list[dict[str, Any]]) -> list[dict[str, Any]]: return merged +def _build_expanded_hunk( + merged_hunk: dict[str, Any], + source_lines: list[str], +) -> tuple[str, list[str]]: + """Build the header and content lines for one expanded hunk. + + Returns: + Tuple of (hunk_header, hunk_lines). + """ + expansion_start = merged_hunk.get("expansion_start") + expansion_end = merged_hunk.get("expansion_end") + original_hunk = merged_hunk.get("original_hunk", merged_hunk) + + if expansion_start is None or expansion_end is None: + return original_hunk["header"], original_hunk["lines"] + + hunk_start_new = merged_hunk.get("hunk_start_new", expansion_start) + hunk_end_new = merged_hunk.get("hunk_end_new", expansion_end) + + new_hunk_lines: list[str] = [] + + # Pre-context + for line_num in range(expansion_start, hunk_start_new): + if line_num <= len(source_lines): + new_hunk_lines.append(f" {source_lines[line_num - 1]}") + + # Diff lines from constituent hunks + for sub_hunk in merged_hunk.get("merged_hunks", [original_hunk]): + new_hunk_lines.extend(sub_hunk.get("lines", [])) + + # Post-context + for line_num in range(hunk_end_new + 1, expansion_end + 1): + if line_num <= len(source_lines): + new_hunk_lines.append(f" {source_lines[line_num - 1]}") + + # Compute counts + new_file_count = sum(1 for l in new_hunk_lines if l.startswith((" ", "+"))) + old_count = sum(1 for l in new_hunk_lines if l.startswith((" ", "-"))) + header = f"@@ -{expansion_start},{old_count} +{expansion_start},{new_file_count} @@" + + return header, new_hunk_lines + + def _rebuild_patch( raw_patch: str, merged_hunks: list[dict[str, Any]], @@ -378,69 +421,15 @@ def _rebuild_patch( """ if not merged_hunks: return None - - # Split original patch to get header lines (before first hunk) lines = raw_patch.split("\n") - header_lines = [] + header_lines: list[str] = [] for line in lines: if line.startswith("@@"): break header_lines.append(line) - - result_lines = list(header_lines) - + result_lines: list[str] = list(header_lines) for merged_hunk in merged_hunks: - expansion_start = merged_hunk.get("expansion_start") - expansion_end = merged_hunk.get("expansion_end") - original_hunk = merged_hunk.get("original_hunk", merged_hunk) - - if expansion_start is None or expansion_end is None: - # No expansion, keep original hunk - result_lines.append(original_hunk["header"]) - result_lines.extend(original_hunk["lines"]) - continue - - # Get the hunk boundaries - hunk_start_new = merged_hunk.get("hunk_start_new", expansion_start) - hunk_end_new = merged_hunk.get("hunk_end_new", expansion_end) - - # Build new hunk lines - new_hunk_lines = [] - - # Add context lines before the original hunk (from expansion_start to hunk_start_new - 1) - pre_context_lines = [] - for line_num in range(expansion_start, hunk_start_new): - if line_num <= len(source_lines): - pre_context_lines.append(f" {source_lines[line_num - 1]}") - new_hunk_lines.extend(pre_context_lines) - - # Add diff lines from all constituent hunks (merged_hunks if present, else original_hunk) - merged_sub_hunks = merged_hunk.get("merged_hunks", [original_hunk]) - for sub_hunk in merged_sub_hunks: - sub_lines = sub_hunk.get("lines", []) - new_hunk_lines.extend(sub_lines) - - # Add context lines after the original hunk (from hunk_end_new + 1 to expansion_end) - post_context_lines = [] - for line_num in range(hunk_end_new + 1, expansion_end + 1): - if line_num <= len(source_lines): - post_context_lines.append(f" {source_lines[line_num - 1]}") - new_hunk_lines.extend(post_context_lines) - - # Calculate new hunk header counts - # For the new file: count context lines and additions - new_file_count = 0 - for line in new_hunk_lines: - if line.startswith(" ") or line.startswith("+"): - new_file_count += 1 - - # For the old file: count context (" ") and deletion ("-") lines - old_count = sum(1 for line in new_hunk_lines if line.startswith(" ") or line.startswith("-")) - - # Build new header - new_header = f"@@ -{expansion_start},{old_count} +{expansion_start},{new_file_count} @@" - - result_lines.append(new_header) - result_lines.extend(new_hunk_lines) - + hunk_header, hunk_lines = _build_expanded_hunk(merged_hunk, source_lines) + result_lines.append(hunk_header) + result_lines.extend(hunk_lines) return "\n".join(result_lines) diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index eb24a3b..6fe3322 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import posixpath from codespy.tools.storage.base import Storage from codespy.tools.storage.models import ( @@ -60,15 +61,12 @@ def __init__( # ------------------------------------------------------------------ def _resolve_path(self, path: str) -> str: - normalised = path.lstrip("/") - parts = normalised.split("/") - resolved: list[str] = [] - for part in parts: - if part == "..": - raise ValueError(f"Path escapes bucket root: {path!r}") - if part and part != ".": - resolved.append(part) - return "/".join(resolved) + normalised = posixpath.normpath(path.strip("/")) + if normalised == ".": + return "" + if normalised.startswith(".."): + raise ValueError(f"Path escapes bucket root: {path!r}") + return normalised def _file_name(self, path: str) -> str: return path.rstrip("/").rsplit("/", 1)[-1] @@ -247,6 +245,12 @@ def read_file( if len(raw) > max_bytes: truncated = True + raw = raw[:max_bytes] + # Back up to a valid UTF-8 character boundary + while raw and (raw[-1] & 0xC0) == 0x80: + raw = raw[:-1] + if raw and raw[-1] >= 0xC0: + raw = raw[:-1] try: content = raw.decode("utf-8") @@ -263,9 +267,6 @@ def read_file( total_lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0) - if truncated: - content = content[:max_bytes] - if max_lines is not None: lines = content.split("\n") if len(lines) > max_lines: diff --git a/tests/test_s3_client.py b/tests/test_s3_client.py new file mode 100644 index 0000000..7fee1be --- /dev/null +++ b/tests/test_s3_client.py @@ -0,0 +1,58 @@ +import pytest +from codespy.tools.storage.s3.client import S3Client + + +class TestResolvePath: + def setup_method(self): + # Patch boto3 — we only test path logic + self.client = S3Client.__new__(S3Client) + + def test_normal_path(self): + assert self.client._resolve_path("foo/bar/baz.txt") == "foo/bar/baz.txt" + + def test_strips_leading_slash(self): + assert self.client._resolve_path("/foo/bar") == "foo/bar" + + def test_collapses_double_slash(self): + assert self.client._resolve_path("foo//bar") == "foo/bar" + + def test_resolves_dot(self): + assert self.client._resolve_path("foo/./bar") == "foo/bar" + + def test_rejects_traversal(self): + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("foo/../../etc/passwd") + + def test_rejects_leading_traversal(self): + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("../secret") + + def test_empty_after_normalization(self): + assert self.client._resolve_path(".") == "" + assert self.client._resolve_path("/") == "" + + +class TestReadFileTruncation: + def test_truncate_preserves_utf8(self): + # "café" = 63 61 66 c3 a9 (5 bytes), max_bytes=4 cuts inside é + raw = "café".encode("utf-8") # 5 bytes + max_bytes = 4 + # Simulate truncation logic + truncated_raw = raw[:max_bytes] # b'caf\xc3' — incomplete é + while truncated_raw and (truncated_raw[-1] & 0xC0) == 0x80: + truncated_raw = truncated_raw[:-1] + if truncated_raw and truncated_raw[-1] >= 0xC0: + truncated_raw = truncated_raw[:-1] + result = truncated_raw.decode("utf-8") + assert result == "caf" # Clean cut before multi-byte char + + def test_truncate_emoji(self): + raw = "hi🎉bye".encode("utf-8") # "hi" (2) + 🎉 (4) + "bye" (3) = 9 bytes + max_bytes = 4 + truncated_raw = raw[:max_bytes] # b'hi\xf0\x9f' — incomplete emoji + while truncated_raw and (truncated_raw[-1] & 0xC0) == 0x80: + truncated_raw = truncated_raw[:-1] + if truncated_raw and truncated_raw[-1] >= 0xC0: + truncated_raw = truncated_raw[:-1] + result = truncated_raw.decode("utf-8") + assert result == "hi" diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index a8184b7..ec07ee3 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -291,5 +291,31 @@ def test_root_does_not_suppress_when_nested_manifests_exist(self): assert "scripts/deploy" in scope_subroots +class TestAssignFilesDeterminism: + """Verify file assignment is deterministic for same-depth scopes.""" + + def test_same_depth_scopes_deterministic(self): + """Scopes at the same depth should assign files consistently.""" + from codespy.agents.reviewer.models import ScopeResult, ScopeType + + # Two scopes at depth 1 (one slash each) + scope_a = ScopeResult(subroot="packages/alpha", scope_type=ScopeType.LIBRARY, reason="test") + scope_b = ScopeResult(subroot="packages/beta", scope_type=ScopeType.LIBRARY, reason="test") + file_alpha = ChangedFile(filename="packages/alpha/index.ts", status=FileStatus.MODIFIED) + file_beta = ChangedFile(filename="packages/beta/index.ts", status=FileStatus.MODIFIED) + + resolver = ScopeResolver() + # Pass scopes in both orders — assignment should be identical + orphans_ab = resolver._assign_files([scope_a, scope_b], [file_alpha, file_beta]) + scope_a.changed_files.clear() + scope_b.changed_files.clear() + orphans_ba = resolver._assign_files([scope_b, scope_a], [file_alpha, file_beta]) + + assert len(orphans_ab) == 0 + assert len(orphans_ba) == 0 + assert scope_a.changed_files == [file_alpha] + assert scope_b.changed_files == [file_beta] + + if __name__ == "__main__": pytest.main([__file__, "-v"]) From ace36724b53e984c0eda319aa69620642b8e650c Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 20:38:18 +0200 Subject: [PATCH 53/79] wip --- src/codespy/agents/dspy_config.py | 50 ++++--------------------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index a540218..25faa35 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -40,35 +40,6 @@ def _resolve_max_tokens(model: str, max_tokens: int) -> int: return min(max_tokens, ceiling) -def _supports_reasoning_effort(model: str) -> bool | None: - """Whether LiteLLM maps ``reasoning_effort`` onto this model's provider. - - Three outcomes, because "unknown" must not be conflated with - "unsupported": - - - ``True`` — LiteLLM knows the model and maps the parameter. - - ``False`` — LiteLLM knows the model and does *not* map it. Sending it - anyway raises ``UnsupportedParamsError`` on every request, because - ``litellm.drop_params`` defaults to False. - - ``None`` — LiteLLM has no parameter list for the model (Ollama, a - proxy, a custom endpoint). There is no published support to check, so - the caller should pass the value through and let the provider decide. - - Args: - model: The LiteLLM model identifier. - - Returns: - True / False when known, None when the model is unmapped. - """ - try: - params = litellm.get_supported_openai_params(model=model) - except Exception: # Unrecognised provider — treat as unmapped. - return None - if params is None: - return None - return "reasoning_effort" in params - - def _supports_cache_control(model: str) -> bool: """Whether the model uses explicit Anthropic-style cache_control markers. @@ -106,13 +77,11 @@ def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: (timeout, retries, output budget, provider-side prompt caching) are applied uniformly. - ``reasoning_effort`` is forwarded to LiteLLM through ``dspy.LM``'s + ``reasoning_effort`` is always forwarded to LiteLLM through ``dspy.LM``'s ``**kwargs``; LiteLLM maps it onto each provider's native parameter (Anthropic ``thinking.budget_tokens``, OpenAI ``reasoning_effort``, - Ollama ``think``, ...). It is omitted for models LiteLLM knows do not - support it: ``litellm.drop_params`` defaults to False, so sending it to - such a model raises on *every* request, and callers log-and-continue on - LLM errors — which would yield an empty review that still exits 0. + Ollama ``think``, ...). ``drop_params=True`` ensures that models/providers + which do not support it gracefully ignore the parameter instead of crashing. ``max_tokens`` must be passed explicitly: omitting it makes LiteLLM fall back to its own 4096 default, which silently truncates responses (DSPy then @@ -132,18 +101,9 @@ def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: "max_tokens": _resolve_max_tokens(config.model, config.max_tokens), "timeout": settings.llm_timeout, "num_retries": settings.llm_retries, + "drop_params": True, + "reasoning_effort": config.reasoning_effort, } - # Only omit the effort when LiteLLM positively reports it unsupported; - # unmapped models (None) still get it, so reasoning is never silently - # disabled for a model that actually honours it. - if _supports_reasoning_effort(config.model) is False: - logger.warning( - f"Model {config.model} does not support reasoning_effort - ignoring " - f"reasoning_effort={config.reasoning_effort}. Sending it would fail " - f"every request to this model." - ) - else: - lm_kwargs["reasoning_effort"] = config.reasoning_effort # Cache system prompts via explicit Anthropic-style cache_control markers. # Only injected for providers that use explicit markers (Anthropic, Bedrock # Anthropic); OpenAI/Gemini have automatic caching that needs no markers. From f8d5f3acc96fdee2867dc1005b5e5b1ecfdc1691 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 21:23:02 +0200 Subject: [PATCH 54/79] wip --- .env.example | 2 -- codespy.yaml | 2 -- src/codespy/agents/dspy_config.py | 24 ++++++++++++------------ src/codespy/config.py | 2 -- src/codespy/config_memory.py | 1 - 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index 3b92c30..2fbee45 100644 --- a/.env.example +++ b/.env.example @@ -209,13 +209,11 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # tasks, so a cheaper tier than code review usually suffices. # Each falls back to the corresponding DEFAULT_* value when unset. # MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 -# MEMORY_DISTILLER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_DISTILLER_REASONING_EFFORT=low # MEMORY_DISTILLER_TEMPERATURE=1 # MEMORY_DISTILLER_MAX_TOKENS=64000 # MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 -# MEMORY_CARTOGRAPHER_EXTRACTION_MODEL=anthropic/claude-haiku-4-5-20251001 # MEMORY_CARTOGRAPHER_REASONING_EFFORT=low # MEMORY_CARTOGRAPHER_TEMPERATURE=1 # MEMORY_CARTOGRAPHER_MAX_TOKENS=64000 diff --git a/codespy.yaml b/codespy.yaml index a94bc16..1eae78b 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -114,13 +114,11 @@ memory: # null inherits the top-level default_* value. distiller: model: null # MEMORY_DISTILLER_MODEL - extraction_model: null # MEMORY_DISTILLER_EXTRACTION_MODEL reasoning_effort: null # MEMORY_DISTILLER_REASONING_EFFORT temperature: null # MEMORY_DISTILLER_TEMPERATURE max_tokens: null # MEMORY_DISTILLER_MAX_TOKENS cartographer: model: null # MEMORY_CARTOGRAPHER_MODEL - extraction_model: null # MEMORY_CARTOGRAPHER_EXTRACTION_MODEL reasoning_effort: null # MEMORY_CARTOGRAPHER_REASONING_EFFORT temperature: null # MEMORY_CARTOGRAPHER_TEMPERATURE max_tokens: null # MEMORY_CARTOGRAPHER_MAX_TOKENS diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 25faa35..e3b33e6 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -136,13 +136,6 @@ def lm_context(name: str): settings = get_settings() llm_config = settings.get_llm_config(name) lm = new_lm(settings, llm_config) - # Override adapter when this module has a different extraction model - defaults = settings.get_llm_config("default") - if llm_config.extraction_model != defaults.extraction_model: - extraction_lm = new_lm( - settings, llm_config.model_copy(update={"model": llm_config.extraction_model}) - ) - return dspy.context(lm=lm, adapter=TwoStepAdapter(extraction_lm)) return dspy.context(lm=lm) @@ -186,11 +179,16 @@ def configure_dspy(settings: Settings) -> None: defaults = settings.get_llm_config("default") lm = new_lm(settings, defaults) - # Extraction LM for TwoStepAdapter's second stage: a smaller/faster model - # that pulls structured fields out of the main LM's free-form response. + # Extraction LM for TwoStepAdapter's second stage: deterministic field extraction + # from the main LM's free-form response. Never uses reasoning; temperature=0.0 for + # deterministic output. extraction_model = defaults.extraction_model extraction_lm = new_lm( - settings, defaults.model_copy(update={"model": extraction_model}) + settings, defaults.model_copy(update={ + "model": extraction_model, + "reasoning_effort": None, + "temperature": 0.0, + }) ) @@ -238,11 +236,13 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: if sig_config.model: models_to_check.add(sig_config.model) - # Check the Hippocampus reflection models (Distiller / Cartographer) + # Global extraction model (if different from default_model) + if settings.extraction_model: + models_to_check.add(settings.extraction_model) + for module in REFLECTION_MODULES: reflection = settings.get_llm_config(module) models_to_check.add(reflection.model) - models_to_check.add(reflection.extraction_model) # Check each model diff --git a/src/codespy/config.py b/src/codespy/config.py index deee891..0e88a2f 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -219,8 +219,6 @@ def get_llm_config(self, name: str) -> LLMSettings: config = self.get_signature_config(name) model = config.model or self.default_model - # Only reflection modules carry their own extraction model; signatures - # share the global one. module_extraction = getattr(config, "extraction_model", None) return LLMSettings( model=model, diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 6f674e2..4606693 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -34,7 +34,6 @@ class ReflectionModuleConfig(BaseModel): """ model: str | None = None # MEMORY__MODEL - extraction_model: str | None = None # MEMORY__EXTRACTION_MODEL reasoning_effort: ReasoningEffort | None = None # MEMORY__REASONING_EFFORT temperature: float | None = None # MEMORY__TEMPERATURE max_tokens: int | None = None # MEMORY__MAX_TOKENS From 8287d264d6ed9a8511199081e54cefe0c0b25709 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 21:37:30 +0200 Subject: [PATCH 55/79] wip --- src/codespy/agents/memory/hippocampus/hippocampus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 91e2236..7d328de 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -243,7 +243,7 @@ def _buffer_and_distill(self, pred: dspy.Prediction, kwargs: dict) -> None: if (self.max_reflects is None or len(self._episode_trajectories) <= self.max_reflects): try: - self._distill(traj, self._make_question(kwargs)) + self._distill(traj, self._episode_question) self._reflected_count += 1 except Exception: logger.warning( From 36f0de2b77eb1429b576daa189f4cdccd372682d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 11 Aug 2026 22:51:18 +0200 Subject: [PATCH 56/79] wip --- poetry.lock | 528 +----------------- pyproject.toml | 2 +- src/codespy/agents/dspy_config.py | 13 +- .../agents/memory/hippocampus/budget.py | 229 ++++---- .../agents/memory/hippocampus/hippocampus.py | 29 +- .../agents/reviewer/modules/code_reviewer.py | 3 +- .../agents/reviewer/modules/scope_resolver.py | 11 +- .../reviewer/modules/supply_chain_auditor.py | 3 +- src/codespy/config.py | 2 +- src/codespy/config_memory.py | 2 +- 10 files changed, 184 insertions(+), 638 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2dde032..7ee8e44 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,25 +167,6 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} -[[package]] -name = "alembic" -version = "1.18.3" -description = "A database migration tool for SQLAlchemy." -optional = false -python-versions = ">=3.10" -files = [ - {file = "alembic-1.18.3-py3-none-any.whl", hash = "sha256:12a0359bfc068a4ecbb9b3b02cf77856033abfdb59e4a5aca08b7eacd7b74ddd"}, - {file = "alembic-1.18.3.tar.gz", hash = "sha256:1212aa3778626f2b0f0aa6dd4e99a5f99b94bd25a0c1ac0bba3be65e081e50b0"}, -] - -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.4.23" -typing-extensions = ">=4.12" - -[package.extras] -tz = ["tzdata"] - [[package]] name = "annotated-types" version = "0.7.0" @@ -215,20 +196,6 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] -[[package]] -name = "asyncer" -version = "0.0.8" -description = "Asyncer, async and await, focused on developer experience." -optional = false -python-versions = ">=3.8" -files = [ - {file = "asyncer-0.0.8-py3-none-any.whl", hash = "sha256:5920d48fc99c8f8f0f1576e1882f5022885589c5fcbc46ce4224ec3e53776eeb"}, - {file = "asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5.0" - [[package]] name = "attrs" version = "25.4.0" @@ -710,23 +677,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "colorlog" -version = "6.10.1" -description = "Add colours to the output of Python's logging module." -optional = false -python-versions = ">=3.6" -files = [ - {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, - {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -development = ["black", "flake8", "mypy", "pytest", "types-colorama"] - [[package]] name = "cryptography" version = "46.0.4" @@ -843,43 +793,42 @@ files = [ [[package]] name = "dspy" -version = "3.1.3" +version = "3.3.0" description = "DSPy" optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "dspy-3.1.3-py3-none-any.whl", hash = "sha256:26f983372ebb284324cc2162458f7bce509ef5ef7b48be4c9f490fa06ea73e37"}, - {file = "dspy-3.1.3.tar.gz", hash = "sha256:e2fd9edc8678e0abcacd5d7b901f37b84a9f48a3c50718fc7fee95a492796019"}, + {file = "dspy-3.3.0-py3-none-any.whl", hash = "sha256:358cbfb15d13246dc4a289bb2350c0ee602260c8a3869f7f63a48a9d2233e48c"}, + {file = "dspy-3.3.0.tar.gz", hash = "sha256:39aa9531391accda8acd7903b52f3c9d2efe462d4bab0c2256db5352e7392754"}, ] [package.dependencies] anyio = "*" -asyncer = "0.0.8" cachetools = ">=5.5.0" -cloudpickle = ">=3.0.0" +cloudpickle = ">=3.1.2" diskcache = ">=5.6.0" -gepa = {version = "0.0.26", extras = ["dspy"]} +gepa = {version = "0.1.1", extras = ["dspy"]} json-repair = ">=0.54.2" -litellm = ">=1.64.0" +litellm = ">=1.65.8" mcp = {version = "*", optional = true, markers = "python_version >= \"3.10\" and extra == \"mcp\""} -numpy = ">=1.26.0" -openai = ">=0.28.1" -optuna = ">=3.4.0" +openai = ">=1.66.2" orjson = ">=3.9.0" pydantic = ">=2.0" regex = ">=2023.10.3" requests = ">=2.31.0" tenacity = ">=8.2.3" tqdm = ">=4.66.1" -xxhash = ">=3.5.0" [package.extras] anthropic = ["anthropic (>=0.18.0,<1.0.0)"] -dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.64.0)", "litellm[proxy] (>=1.64.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "ruff (>=0.3.0)"] -langchain = ["langchain_core"] +dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.65.8)", "litellm[proxy] (>=1.65.8)", "numpy (>=1.26.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "pytest-xdist (>=3.5.0)", "ruff (>=0.3.0)"] +langchain = ["langchain_core (>=0.3.0)"] +litellm = ["litellm (>=1.65.8)"] mcp = ["mcp"] -test-extras = ["datasets (>=2.14.6)", "langchain_core", "mcp", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] -weaviate = ["weaviate-client (>=4.5.4,<4.6.0)"] +numpy = ["numpy (>=1.26.0)"] +optuna = ["optuna (>=3.4.0)"] +test-extras = ["datasets (>=2.14.6)", "langchain_core (>=0.3.0)", "mcp", "numpy (>=1.26.0)", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] +weaviate = ["weaviate-client (>=4.5.4,<4.22.0)"] [[package]] name = "fake-useragent" @@ -1170,19 +1119,20 @@ tqdm = ["tqdm"] [[package]] name = "gepa" -version = "0.0.26" +version = "0.1.1" description = "A framework for optimizing textual system components (AI prompts, code snippets, etc.) using LLM-based reflection and Pareto-efficient evolutionary search." optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "gepa-0.0.26-py3-none-any.whl", hash = "sha256:331e40d8693a4192de2eb3b2b4df10d410ead49173f748d50c32a035cf746e63"}, - {file = "gepa-0.0.26.tar.gz", hash = "sha256:0119ca8022e93b6236bc154a57bb910bdb117485dc067d77777933dd3e9e9ad8"}, + {file = "gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466"}, + {file = "gepa-0.1.1.tar.gz", hash = "sha256:643fda01c23de4c9f01306e01305dd69facc29bcb34ad59e4cd07e6621d34aa1"}, ] [package.extras] build = ["build", "packaging", "requests", "semver", "setuptools (>=77.0.1)", "twine", "wheel"] dev = ["build (>=1.0.3)", "gepa[build]", "gepa[test]", "pre-commit", "ruff (>=0.3.0)"] -full = ["datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] +full = ["cloudpickle (>=3.0.0)", "datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] +gskill = ["docker", "gepa[full]", "python-dotenv", "pyyaml", "swesmith"] test = ["gepa[full]", "pyright", "pytest"] [[package]] @@ -1217,72 +1167,6 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy (==1.18.2)", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] -[[package]] -name = "greenlet" -version = "3.3.1" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.10" -files = [ - {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4"}, - {file = "greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8"}, - {file = "greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f"}, - {file = "greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b"}, - {file = "greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4"}, - {file = "greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149"}, - {file = "greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a"}, - {file = "greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1"}, - {file = "greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2"}, - {file = "greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946"}, - {file = "greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d"}, - {file = "greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1"}, - {file = "greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a"}, - {file = "greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79"}, - {file = "greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249"}, - {file = "greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451"}, - {file = "greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil", "setuptools"] - [[package]] name = "h11" version = "0.16.0" @@ -1968,25 +1852,6 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -[[package]] -name = "mako" -version = "1.3.10" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, - {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, -] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -2392,87 +2257,6 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] -[[package]] -name = "numpy" -version = "2.4.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -files = [ - {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73"}, - {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1"}, - {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32"}, - {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390"}, - {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413"}, - {file = "numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda"}, - {file = "numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695"}, - {file = "numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27"}, - {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548"}, - {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f"}, - {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460"}, - {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba"}, - {file = "numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f"}, - {file = "numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85"}, - {file = "numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef"}, - {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7"}, - {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499"}, - {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb"}, - {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7"}, - {file = "numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110"}, - {file = "numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622"}, - {file = "numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab"}, - {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82"}, - {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f"}, - {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554"}, - {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257"}, - {file = "numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657"}, - {file = "numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b"}, - {file = "numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74"}, - {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a"}, - {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325"}, - {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909"}, - {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a"}, - {file = "numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a"}, - {file = "numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75"}, - {file = "numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d"}, - {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8"}, - {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5"}, - {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e"}, - {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a"}, - {file = "numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443"}, - {file = "numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236"}, - {file = "numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0"}, - {file = "numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae"}, -] - [[package]] name = "openai" version = "2.16.0" @@ -2500,32 +2284,6 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] -[[package]] -name = "optuna" -version = "4.7.0" -description = "A hyperparameter optimization framework" -optional = false -python-versions = ">=3.9" -files = [ - {file = "optuna-4.7.0-py3-none-any.whl", hash = "sha256:e41ec84018cecc10eabf28143573b1f0bde0ba56dba8151631a590ecbebc1186"}, - {file = "optuna-4.7.0.tar.gz", hash = "sha256:d91817e2079825557bd2e97de2e8c9ae260bfc99b32712502aef8a5095b2d2c0"}, -] - -[package.dependencies] -alembic = ">=1.5.0" -colorlog = "*" -numpy = "*" -packaging = ">=20.0" -PyYAML = "*" -sqlalchemy = ">=1.4.2" -tqdm = "*" - -[package.extras] -checking = ["mypy", "mypy_boto3_s3", "ruff", "scipy-stubs", "types-PyYAML", "types-redis", "types-setuptools", "types-tqdm", "typing_extensions (>=3.10.0.0)"] -document = ["ase", "cmaes (>=0.12.0)", "fvcore", "kaleido (<0.4)", "lightgbm", "matplotlib (!=3.6.0)", "pandas", "pillow", "plotly (>=4.9.0)", "scikit-learn", "sphinx", "sphinx-copybutton", "sphinx-gallery", "sphinx-notfound-page", "sphinx_rtd_theme (>=1.2.0)", "torch", "torchvision"] -optional = ["boto3", "cmaes (>=0.12.0)", "google-cloud-storage", "greenlet", "grpcio", "matplotlib (!=3.6.0)", "pandas", "plotly (>=4.9.0)", "protobuf (>=5.28.1)", "redis", "scikit-learn (>=0.24.2)", "scipy", "torch"] -test = ["fakeredis[lua]", "greenlet", "grpcio", "kaleido (<0.4)", "moto", "protobuf (>=5.28.1)", "pytest", "pytest-xdist", "scipy (>=1.9.2)", "torch"] - [[package]] name = "orjson" version = "3.11.7" @@ -3739,103 +3497,6 @@ files = [ {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, ] -[[package]] -name = "sqlalchemy" -version = "2.0.46" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ac245604295b521de49b465bab845e3afe6916bcb2147e5929c8041b4ec0545"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e6199143d51e3e1168bedd98cc698397404a8f7508831b81b6a29b18b051069"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:716be5bcabf327b6d5d265dbdc6213a01199be587224eb991ad0d37e83d728fd"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6f827fd687fa1ba7f51699e1132129eac8db8003695513fcf13fc587e1bd47a5"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c805fa6e5d461329fa02f53f88c914d189ea771b6821083937e79550bf31fc19"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-win32.whl", hash = "sha256:3aac08f7546179889c62b53b18ebf1148b10244b3405569c93984b0388d016a7"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-win_amd64.whl", hash = "sha256:0cc3117db526cad3e61074100bd2867b533e2c7dc1569e95c14089735d6fb4fe"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:90bde6c6b1827565a95fde597da001212ab436f1b2e0c2dcc7246e14db26e2a3"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b1e5f3a5f1ff4f42d5daab047428cd45a3380e51e191360a35cef71c9a7a2a"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93bb0aae40b52c57fd74ef9c6933c08c040ba98daf23ad33c3f9893494b8d3ce"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4e2cc868b7b5208aec6c960950b7bb821f82c2fe66446c92ee0a571765e91a5"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:965c62be8256d10c11f8907e7a8d3e18127a4c527a5919d85fa87fd9ecc2cfdc"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-win32.whl", hash = "sha256:9397b381dcee8a2d6b99447ae85ea2530dcac82ca494d1db877087a13e38926d"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl", hash = "sha256:4396c948d8217e83e2c202fbdcc0389cf8c93d2c1c5e60fa5c5a955eae0e64be"}, - {file = "sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e"}, - {file = "sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7"}, -] - -[package.dependencies] -greenlet = {version = ">=1", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] -aioodbc = ["aioodbc", "greenlet (>=1)"] -aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (>=1)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - [[package]] name = "sse-starlette" version = "3.2.0" @@ -4372,155 +4033,6 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] -[[package]] -name = "xxhash" -version = "3.6.0" -description = "Python binding for xxHash" -optional = false -python-versions = ">=3.7" -files = [ - {file = "xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71"}, - {file = "xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b"}, - {file = "xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b"}, - {file = "xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}, - {file = "xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3"}, - {file = "xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd"}, - {file = "xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef"}, - {file = "xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae"}, - {file = "xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb"}, - {file = "xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c"}, - {file = "xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd"}, - {file = "xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799"}, - {file = "xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392"}, - {file = "xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033"}, - {file = "xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5"}, - {file = "xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f"}, - {file = "xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad"}, - {file = "xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518"}, - {file = "xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b"}, - {file = "xxhash-3.6.0-cp38-cp38-win32.whl", hash = "sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536"}, - {file = "xxhash-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796"}, - {file = "xxhash-3.6.0-cp39-cp39-win32.whl", hash = "sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d"}, - {file = "xxhash-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802"}, - {file = "xxhash-3.6.0-cp39-cp39-win_arm64.whl", hash = "sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d"}, - {file = "xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6"}, -] - [[package]] name = "yarl" version = "1.22.0" @@ -4687,4 +4199,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "9a3b7297f4254b101ea7184fad127421f243734e6923014c393615f87f7f7db6" +content-hash = "b268e92061ffd1720bf8ed5252a00e6f0132e8d9e8df7b78ac3be3e492e26103" diff --git a/pyproject.toml b/pyproject.toml index b802600..a1d54a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.11,<3.14" -dspy = {version = "^3.1.3", extras = ["mcp"]} +dspy = {version = "^3.3.0", extras = ["mcp"]} litellm = "^1.81.6" cachetools = ">=5.0.0" PyGithub = ">=2.5.0" diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index e3b33e6..777e5c6 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -1,15 +1,24 @@ """DSPy and LiteLLM configuration utilities.""" +import asyncio import logging import dspy # type: ignore[import-untyped] from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] +from dspy.predict.react_v2 import ReActV2 as _ReActV2 # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] from codespy.config import Settings, get_settings from codespy.config_memory import LLMSettings, REFLECTION_MODULES +class AsyncReActV2(_ReActV2): + """ReActV2 with aforward() for async callers (acall).""" + + async def aforward(self, **kwargs): + return await asyncio.to_thread(self.forward, **kwargs) + + logger = logging.getLogger(__name__) @@ -153,7 +162,7 @@ def configure_dspy(settings: Settings) -> None: - Memory caching for LLM responses TwoStepAdapter decouples reasoning quality from format compliance, - solving ChatAdapter parsing failures with ReAct agents. + solving ChatAdapter parsing failures with ReActV2 agents. Args: settings: Application settings containing model and API key configuration. @@ -194,7 +203,7 @@ def configure_dspy(settings: Settings) -> None: dspy.settings.configure( lm=lm, - adapter=TwoStepAdapter(extraction_lm), # TwoStepAdapter solves ChatAdapter parsing failures + adapter=TwoStepAdapter(extraction_lm, use_native_function_calling=True), # TwoStepAdapter solves ChatAdapter parsing failures ) # Enable memory-only caching for LLM calls (no disk caching) diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index b4bf238..4357acc 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -139,19 +139,6 @@ def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: # Trajectory formatting with optional step-aware head+tail bounding # --------------------------------------------------------------------------- -def _format_step(i: int, entry: dict) -> str: - parts = [f"--- Step {i + 1} ---"] - if entry.get("reasoning"): - parts.append(f"Reasoning: {entry['reasoning']}") - code = entry.get("code", "") - if code: - parts.append(f"Code:\n{code}") - output = entry.get("output", "") - if output: - parts.append(f"Output:\n{output}") - return "\n".join(parts) - - # Tokens set aside for the "... (N tokens omitted) ..." marker so the returned # text honours max_tokens including the marker itself. _MARKER_RESERVE = 16 @@ -225,108 +212,140 @@ def _head_tail_text(text: str, max_tokens: int, head_ratio: float = 0.6) -> str: return "".join(head_lines) + marker + "".join(tail_lines) +def _format_step(i: int, entry: dict) -> str: + """Format a single step dict (CodeAct style) as readable text.""" + parts = [f"--- Step {i + 1} ---"] + if entry.get("reasoning"): + parts.append(f"Reasoning: {entry['reasoning']}") + code = entry.get("code", "") + if code: + parts.append(f"Code:\n{code}") + output = entry.get("output", "") + if output: + parts.append(f"Output:\n{output}") + return "\n".join(parts) + + +def _bound_steps(step_texts: list[str], max_tokens: int | None) -> str: + """Step-aware head/tail bounding for a list of formatted step strings. + + Keeps whole steps from front (60%) and back (40%), replaces middle with + omission marker. Caps oversized individual steps first. + """ + if max_tokens is None: + return "\n\n".join(step_texts) + + full = "\n\n".join(step_texts) + if count_tokens(full) <= max_tokens: + return full + + content_budget = max(max_tokens - _MARKER_RESERVE, 0) + head_budget = int(content_budget * 0.6) + tail_budget = content_budget - head_budget + + capped = [_head_tail_text(s, max_tokens) if count_tokens(s) > max_tokens else s for s in step_texts] + + head_steps: list[str] = [] + head_tokens = 0 + for s in capped: + t = count_tokens(s) + if head_tokens + t > head_budget: + break + head_steps.append(s) + head_tokens += t + + tail_steps: list[str] = [] + tail_tokens = 0 + for s in reversed(capped[len(head_steps):]): + t = count_tokens(s) + if tail_tokens + t > tail_budget: + break + tail_steps.append(s) + tail_tokens += t + tail_steps.reverse() + + if not head_steps and capped: + head_steps = [_head_tail_text(capped[0], head_budget)] + if not tail_steps and len(capped) > len(head_steps): + tail_steps = [_head_tail_text(capped[-1], tail_budget)] + + n_omitted = len(capped) - len(head_steps) - len(tail_steps) + parts = list(head_steps) + if n_omitted > 0: + first_omitted = len(head_steps) + 1 + last_omitted = len(capped) - len(tail_steps) + parts.append(f"--- Steps {first_omitted}–{last_omitted} omitted ({n_omitted} steps) ---") + parts.extend(tail_steps) + return "\n\n".join(parts) + + +def _format_list_traj(traj: list[dict], max_tokens: int | None) -> str: + """Format a CodeAct list trajectory with step-aware bounding.""" + step_texts = [_format_step(i, entry) for i, entry in enumerate(traj)] + return _bound_steps(step_texts, max_tokens) + + +def _format_history_event(i: int, event: dict) -> str: + """Format one ReActV2 history turn as readable text.""" + parts = [f"--- Turn {i + 1} ---"] + if "next_thought" in event: + parts.append(f"Thought: {event['next_thought']}") + if "tool_calls" in event: + tc = event["tool_calls"] + if hasattr(tc, "tool_calls"): + for call in tc.tool_calls: + call_str = f"Tool: {call.name}({call.args or {}})" + if hasattr(tc, "tool_call_results") and tc.tool_call_results: + results = getattr(tc.tool_call_results, "tool_call_results", []) or [] + matching = [r for r in results if getattr(r, "call_id", None) == call.id] + if matching: + call_str += f"\n -> {matching[0].value}" + parts.append(call_str) + else: + parts.append(f"ToolCalls: {tc}") + for k, v in event.items(): + if k not in ("next_thought", "tool_calls"): + parts.append(f"{k}: {v}") + return "\n".join(parts) + + +def _format_history(history, max_tokens: int | None) -> str: + """Format a dspy.History (ReActV2) with step-aware bounding.""" + messages = history.messages if hasattr(history, "messages") else [] + step_texts = [ + _format_history_event(i, ev) if isinstance(ev, dict) else f"--- Turn {i + 1} ---\n{ev}" + for i, ev in enumerate(messages) + ] + return _bound_steps(step_texts, max_tokens) + + def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> str: - """Serialize a dspy trajectory to text, with optional head+tail bounding. - - Args: - pred: The dspy Prediction returned by the wrapped agent. - max_tokens: If None (default), the full trajectory is returned so the - Distiller can do all compression. If set, step-aware head+tail - bounding is applied: whole steps are kept from both the front and - back of the trajectory (60 % head / 40 % tail), and the middle is - replaced by an omission marker. For dict / fallback trajectories, - the same head+tail logic is applied at line granularity. A single - oversized step's Output block is itself head+tail bounded before - the per-step budget accounting. - - Trajectory shapes handled: - list — ReAct / CodeAct: list of dicts with 'code', 'output', - optional 'reasoning'. Step-aware bounding. - dict — flat key/value dump. Line-granularity bounding. - other — str(pred) or pred.toDict() fallback. Line-granularity bounding. + """Serialize a prediction's execution trace to bounded text. + + Dispatches by prediction shape: + history (dspy.History) - ReActV2: structured turn messages + trajectory (list) - CodeAct: list of step dicts + trajectory (dict) - legacy ReAct: flat key/value dump + fallback - pred.toDict() or str(pred) """ + # ReActV2: history attribute + history = getattr(pred, "history", None) + if history is not None and hasattr(history, "messages"): + return _format_history(history, max_tokens) + + # Legacy: trajectory attribute traj = getattr(pred, "trajectory", None) - # ----- list path (ReAct / CodeAct) ----- if isinstance(traj, list): - step_texts = [_format_step(i, entry) for i, entry in enumerate(traj)] - - if max_tokens is None: - return "\n\n".join(step_texts) - - # Check if everything fits as-is - full = "\n\n".join(step_texts) - if count_tokens(full) <= max_tokens: - return full - - content_budget = max(max_tokens - _MARKER_RESERVE, 0) - head_budget = int(content_budget * 0.6) - tail_budget = content_budget - head_budget - - # Cap individual oversized step outputs before budgeting - capped: list[str] = [] - for s in step_texts: - if count_tokens(s) > max_tokens: - s = _head_tail_text(s, max_tokens) - capped.append(s) - - # Greedily keep head steps - head_steps: list[str] = [] - head_tokens = 0 - for s in capped: - t = count_tokens(s) - if head_tokens + t > head_budget: - break - head_steps.append(s) - head_tokens += t - - # Greedily keep tail steps (from the end), never reusing a head step - tail_steps: list[str] = [] - tail_tokens = 0 - for s in reversed(capped[len(head_steps):]): - t = count_tokens(s) - if tail_tokens + t > tail_budget: - break - tail_steps.append(s) - tail_tokens += t - tail_steps.reverse() - - # If no whole step fits in either half (every step is larger than its - # budget), fall back to bounding single steps so the budget is actually - # used instead of returning just the omission marker. - if not head_steps and capped: - head_steps = [_head_tail_text(capped[0], head_budget)] - if not tail_steps and len(capped) > len(head_steps): - tail_steps = [_head_tail_text(capped[-1], tail_budget)] - - # Determine omitted range - n_head = len(head_steps) - n_tail = len(tail_steps) - n_total = len(capped) - n_omitted = n_total - n_head - n_tail - - parts = list(head_steps) - if n_omitted > 0: - first_omitted = n_head + 1 - last_omitted = n_total - n_tail - parts.append( - f"--- Steps {first_omitted}–{last_omitted} omitted ({n_omitted} steps) ---" - ) - parts.extend(tail_steps) - return "\n\n".join(parts) + return _format_list_traj(traj, max_tokens) - # ----- dict path ----- if isinstance(traj, dict): text = "\n".join(f"{k}: {v}" for k, v in traj.items()) - if max_tokens is None: - return text - return _head_tail_text(text, max_tokens) + return text if max_tokens is None else _head_tail_text(text, max_tokens) - # ----- fallback ----- + # Fallback try: text = "\n".join(f"{k}: {v}" for k, v in pred.toDict().items()) except Exception: text = str(pred) - if max_tokens is None: - return text - return _head_tail_text(text, max_tokens) + return text if max_tokens is None else _head_tail_text(text, max_tokens) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 7d328de..b8ac785 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -118,7 +118,7 @@ def __init__( ): """ Args: - module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. + module: Any dspy.Module (ReActV2, RLM, Predict, …) to wrap. budget: The four token budgets bounding memory, as a :class:`MemoryBudget`. Defaults to ``MemoryBudget()`` — see that class for per-field guidance. Resolve one from configuration with @@ -134,14 +134,14 @@ class for per-field guidance. Resolve one from configuration with If None, all input fields are serialized (bounded by ``budget.max_question_tokens``). Set this when one field cleanly captures user intent. - task_name: Identity recorded in ``Episode.task`` and used in the episode - filename. Pass the signature's snake_case name (``"doc"``, - ``"code_review"``, …) — the same key that drives config, LM - selection and cost attribution — so the episode path lines up with - the rest of the system. Inference is a last resort: only - ``dspy.ReAct``-style modules expose ``.signature``, - ``dspy.ChainOfThought`` does not, so the fallback would yield a - meaningless (and collision-prone) ``"ChainOfThought"``. + task_name: Identity recorded in ``Episode.task`` and used in the episode + filename. Pass the signature's snake_case name (``"doc"``, + ``"code_review"``, …) — the same key that drives config, LM + selection and cost attribution — so the episode path lines up with + the rest of the system. Inference is a last resort: only + ``dspy.ReActV2``-style modules expose ``.signature``, + ``dspy.ChainOfThought`` does not, so the fallback would yield a + meaningless (and collision-prone) ``"ChainOfThought"``. run_id: Identifier of the pipeline run this agent belongs to. Passed down by the orchestrating ``ReviewPipeline`` so every module invoked within the same review run shares the same identifier, @@ -163,12 +163,15 @@ class for per-field guidance. Resolve one from configuration with module_inputs = set(top_sig.input_fields) module.signature = prepend_context_map(top_sig) for _, pred in module.named_predictors(): - if set(pred.signature.input_fields) & module_inputs: - if "context_map" not in pred.signature.input_fields: - pred.signature = prepend_context_map(pred.signature) + pred_sig = getattr(pred, "signature", None) + if pred_sig is not None and set(pred_sig.input_fields) & module_inputs: + if "context_map" not in pred_sig.input_fields: + pred.signature = prepend_context_map(pred_sig) else: for _, pred in module.named_predictors(): - pred.signature = prepend_context_map(pred.signature) + pred_sig = getattr(pred, "signature", None) + if pred_sig is not None: + pred.signature = prepend_context_map(pred_sig) self.agent = module self.distill = Distiller() diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 4d46206..5d5d532 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult @@ -228,7 +229,7 @@ async def aforward( scope_root = resolve_scope_root(repo_path, scope.subroot) tools, contexts = await self._create_tools(scope_root) try: - agent = dspy.ReAct( + agent = AsyncReActV2( signature=CodeReviewSignature, tools=tools, max_iters=max_iters, diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 8ad5a96..aedc263 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -1,6 +1,6 @@ -"""Scope resolver module - merged deterministic analysis + ReAct agent refinement. +"""Scope resolver module - merged deterministic analysis + ReActV2 agent refinement. -This module combines deterministic scope identification with a ReAct agent +This module combines deterministic scope identification with a ReActV2 agent for intelligent refinement. The agent uses filesystem and search tools to explore the codebase and make informed scope decisions, replacing the previous ChainOfThought predictor that relied on a static repo tree. @@ -19,6 +19,7 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import ContextMap, Hippocampus from codespy.agents.reviewer.models import ( PackageManifest, @@ -363,7 +364,7 @@ def derive_sparse_paths(changed_files: list[str]) -> list[str]: # For glob patterns like *.csproj, we need to add the pattern itself paths.append(manifest_pattern) - # Agent config directories — project instructions for ReAct agents + # Agent config directories — project instructions for ReActV2 agents paths.extend([ ".claude/", ".kilo/", @@ -878,7 +879,7 @@ async def _refine_scopes( review_context: ReviewContext | None, run_id: str | None, ) -> list[ScopeResult]: - """Use ReAct agent to refine scope assignments from deterministic candidates. + """Use ReActV2 agent to refine scope assignments from deterministic candidates. Args: scopes: Already-resolved scope results @@ -900,7 +901,7 @@ async def _refine_scopes( max_iters = self._settings.get_max_iters("scope") tools, contexts = await self._create_tools(repo_path) try: - agent = dspy.ReAct( + agent = AsyncReActV2( signature=ScopeRefinementSignature, tools=tools, max_iters=max_iters, diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index fd7ba22..ee4ffec 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult @@ -313,7 +314,7 @@ async def aforward( try: # Combine scoped filesystem tools with shared OSV tools all_tools = scoped_tools + osv_tools - supply_chain_agent = dspy.ReAct( + supply_chain_agent = AsyncReActV2( signature=SupplyChainSecuritySignature, tools=all_tools, max_iters=supply_chain_max_iters, diff --git a/src/codespy/config.py b/src/codespy/config.py index 0e88a2f..3fad410 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -265,7 +265,7 @@ def get_memory_max_context_map_tokens(self, signature_name: str) -> int: Bounds the rendered ContextMap — the persisted artifact that is prepended to every predictor of the wrapped agent, and therefore re-sent on every - ReAct iteration. + ReActV2 iteration. """ config = self.get_signature_config(signature_name).memory return ( diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 4606693..e7cbfb8 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -95,7 +95,7 @@ class MemoryConfig(BaseModel): # Ceiling on the rendered ContextMap. This is the *persisted* artifact and it # is prepended to every predictor of the wrapped agent, so it is re-sent on - # every ReAct iteration (~default_max_iters times per scope) plus once per + # every ReActV2 iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. # Approximate item capacity is default_max_context_map_tokens divided by # default_max_context_item_tokens (3072 / 240 ~= 12 items). From dc5cd8be260468d1f01a2aa8eacd3228cff4fbbf Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Wed, 12 Aug 2026 09:05:16 +0200 Subject: [PATCH 57/79] Revert "wip" This reverts commit 36f0de2b77eb1429b576daa189f4cdccd372682d. --- poetry.lock | 528 +++++++++++++++++- pyproject.toml | 2 +- src/codespy/agents/dspy_config.py | 13 +- .../agents/memory/hippocampus/budget.py | 229 ++++---- .../agents/memory/hippocampus/hippocampus.py | 29 +- .../agents/reviewer/modules/code_reviewer.py | 3 +- .../agents/reviewer/modules/scope_resolver.py | 11 +- .../reviewer/modules/supply_chain_auditor.py | 3 +- src/codespy/config.py | 2 +- src/codespy/config_memory.py | 2 +- 10 files changed, 638 insertions(+), 184 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7ee8e44..2dde032 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,6 +167,25 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} +[[package]] +name = "alembic" +version = "1.18.3" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.10" +files = [ + {file = "alembic-1.18.3-py3-none-any.whl", hash = "sha256:12a0359bfc068a4ecbb9b3b02cf77856033abfdb59e4a5aca08b7eacd7b74ddd"}, + {file = "alembic-1.18.3.tar.gz", hash = "sha256:1212aa3778626f2b0f0aa6dd4e99a5f99b94bd25a0c1ac0bba3be65e081e50b0"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.4.23" +typing-extensions = ">=4.12" + +[package.extras] +tz = ["tzdata"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -196,6 +215,20 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] +[[package]] +name = "asyncer" +version = "0.0.8" +description = "Asyncer, async and await, focused on developer experience." +optional = false +python-versions = ">=3.8" +files = [ + {file = "asyncer-0.0.8-py3-none-any.whl", hash = "sha256:5920d48fc99c8f8f0f1576e1882f5022885589c5fcbc46ce4224ec3e53776eeb"}, + {file = "asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5.0" + [[package]] name = "attrs" version = "25.4.0" @@ -677,6 +710,23 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "colorlog" +version = "6.10.1" +description = "Add colours to the output of Python's logging module." +optional = false +python-versions = ">=3.6" +files = [ + {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, + {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +development = ["black", "flake8", "mypy", "pytest", "types-colorama"] + [[package]] name = "cryptography" version = "46.0.4" @@ -793,42 +843,43 @@ files = [ [[package]] name = "dspy" -version = "3.3.0" +version = "3.1.3" description = "DSPy" optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "dspy-3.3.0-py3-none-any.whl", hash = "sha256:358cbfb15d13246dc4a289bb2350c0ee602260c8a3869f7f63a48a9d2233e48c"}, - {file = "dspy-3.3.0.tar.gz", hash = "sha256:39aa9531391accda8acd7903b52f3c9d2efe462d4bab0c2256db5352e7392754"}, + {file = "dspy-3.1.3-py3-none-any.whl", hash = "sha256:26f983372ebb284324cc2162458f7bce509ef5ef7b48be4c9f490fa06ea73e37"}, + {file = "dspy-3.1.3.tar.gz", hash = "sha256:e2fd9edc8678e0abcacd5d7b901f37b84a9f48a3c50718fc7fee95a492796019"}, ] [package.dependencies] anyio = "*" +asyncer = "0.0.8" cachetools = ">=5.5.0" -cloudpickle = ">=3.1.2" +cloudpickle = ">=3.0.0" diskcache = ">=5.6.0" -gepa = {version = "0.1.1", extras = ["dspy"]} +gepa = {version = "0.0.26", extras = ["dspy"]} json-repair = ">=0.54.2" -litellm = ">=1.65.8" +litellm = ">=1.64.0" mcp = {version = "*", optional = true, markers = "python_version >= \"3.10\" and extra == \"mcp\""} -openai = ">=1.66.2" +numpy = ">=1.26.0" +openai = ">=0.28.1" +optuna = ">=3.4.0" orjson = ">=3.9.0" pydantic = ">=2.0" regex = ">=2023.10.3" requests = ">=2.31.0" tenacity = ">=8.2.3" tqdm = ">=4.66.1" +xxhash = ">=3.5.0" [package.extras] anthropic = ["anthropic (>=0.18.0,<1.0.0)"] -dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.65.8)", "litellm[proxy] (>=1.65.8)", "numpy (>=1.26.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "pytest-xdist (>=3.5.0)", "ruff (>=0.3.0)"] -langchain = ["langchain_core (>=0.3.0)"] -litellm = ["litellm (>=1.65.8)"] +dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.64.0)", "litellm[proxy] (>=1.64.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "ruff (>=0.3.0)"] +langchain = ["langchain_core"] mcp = ["mcp"] -numpy = ["numpy (>=1.26.0)"] -optuna = ["optuna (>=3.4.0)"] -test-extras = ["datasets (>=2.14.6)", "langchain_core (>=0.3.0)", "mcp", "numpy (>=1.26.0)", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] -weaviate = ["weaviate-client (>=4.5.4,<4.22.0)"] +test-extras = ["datasets (>=2.14.6)", "langchain_core", "mcp", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] +weaviate = ["weaviate-client (>=4.5.4,<4.6.0)"] [[package]] name = "fake-useragent" @@ -1119,20 +1170,19 @@ tqdm = ["tqdm"] [[package]] name = "gepa" -version = "0.1.1" +version = "0.0.26" description = "A framework for optimizing textual system components (AI prompts, code snippets, etc.) using LLM-based reflection and Pareto-efficient evolutionary search." optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466"}, - {file = "gepa-0.1.1.tar.gz", hash = "sha256:643fda01c23de4c9f01306e01305dd69facc29bcb34ad59e4cd07e6621d34aa1"}, + {file = "gepa-0.0.26-py3-none-any.whl", hash = "sha256:331e40d8693a4192de2eb3b2b4df10d410ead49173f748d50c32a035cf746e63"}, + {file = "gepa-0.0.26.tar.gz", hash = "sha256:0119ca8022e93b6236bc154a57bb910bdb117485dc067d77777933dd3e9e9ad8"}, ] [package.extras] build = ["build", "packaging", "requests", "semver", "setuptools (>=77.0.1)", "twine", "wheel"] dev = ["build (>=1.0.3)", "gepa[build]", "gepa[test]", "pre-commit", "ruff (>=0.3.0)"] -full = ["cloudpickle (>=3.0.0)", "datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] -gskill = ["docker", "gepa[full]", "python-dotenv", "pyyaml", "swesmith"] +full = ["datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] test = ["gepa[full]", "pyright", "pytest"] [[package]] @@ -1167,6 +1217,72 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy (==1.18.2)", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +[[package]] +name = "greenlet" +version = "3.3.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +files = [ + {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, + {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, + {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5"}, + {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5"}, + {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe"}, + {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729"}, + {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4"}, + {file = "greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8"}, + {file = "greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c"}, + {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd"}, + {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5"}, + {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f"}, + {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2"}, + {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9"}, + {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f"}, + {file = "greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b"}, + {file = "greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4"}, + {file = "greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975"}, + {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36"}, + {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba"}, + {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca"}, + {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336"}, + {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1"}, + {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149"}, + {file = "greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a"}, + {file = "greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1"}, + {file = "greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3"}, + {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac"}, + {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd"}, + {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e"}, + {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3"}, + {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951"}, + {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2"}, + {file = "greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946"}, + {file = "greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d"}, + {file = "greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5"}, + {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b"}, + {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e"}, + {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d"}, + {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f"}, + {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683"}, + {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1"}, + {file = "greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a"}, + {file = "greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79"}, + {file = "greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242"}, + {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774"}, + {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97"}, + {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab"}, + {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2"}, + {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53"}, + {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249"}, + {file = "greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451"}, + {file = "greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + [[package]] name = "h11" version = "0.16.0" @@ -1852,6 +1968,25 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] +[[package]] +name = "mako" +version = "1.3.10" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, + {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -2257,6 +2392,87 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "numpy" +version = "2.4.2" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +files = [ + {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7"}, + {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73"}, + {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1"}, + {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32"}, + {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390"}, + {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413"}, + {file = "numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda"}, + {file = "numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695"}, + {file = "numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e"}, + {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27"}, + {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548"}, + {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f"}, + {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460"}, + {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba"}, + {file = "numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f"}, + {file = "numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85"}, + {file = "numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98"}, + {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef"}, + {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7"}, + {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499"}, + {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb"}, + {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7"}, + {file = "numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110"}, + {file = "numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622"}, + {file = "numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913"}, + {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab"}, + {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82"}, + {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f"}, + {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554"}, + {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257"}, + {file = "numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657"}, + {file = "numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b"}, + {file = "numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1"}, + {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74"}, + {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a"}, + {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325"}, + {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909"}, + {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a"}, + {file = "numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a"}, + {file = "numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75"}, + {file = "numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef"}, + {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d"}, + {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8"}, + {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5"}, + {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e"}, + {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a"}, + {file = "numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443"}, + {file = "numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236"}, + {file = "numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0"}, + {file = "numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0"}, + {file = "numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae"}, +] + [[package]] name = "openai" version = "2.16.0" @@ -2284,6 +2500,32 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] +[[package]] +name = "optuna" +version = "4.7.0" +description = "A hyperparameter optimization framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "optuna-4.7.0-py3-none-any.whl", hash = "sha256:e41ec84018cecc10eabf28143573b1f0bde0ba56dba8151631a590ecbebc1186"}, + {file = "optuna-4.7.0.tar.gz", hash = "sha256:d91817e2079825557bd2e97de2e8c9ae260bfc99b32712502aef8a5095b2d2c0"}, +] + +[package.dependencies] +alembic = ">=1.5.0" +colorlog = "*" +numpy = "*" +packaging = ">=20.0" +PyYAML = "*" +sqlalchemy = ">=1.4.2" +tqdm = "*" + +[package.extras] +checking = ["mypy", "mypy_boto3_s3", "ruff", "scipy-stubs", "types-PyYAML", "types-redis", "types-setuptools", "types-tqdm", "typing_extensions (>=3.10.0.0)"] +document = ["ase", "cmaes (>=0.12.0)", "fvcore", "kaleido (<0.4)", "lightgbm", "matplotlib (!=3.6.0)", "pandas", "pillow", "plotly (>=4.9.0)", "scikit-learn", "sphinx", "sphinx-copybutton", "sphinx-gallery", "sphinx-notfound-page", "sphinx_rtd_theme (>=1.2.0)", "torch", "torchvision"] +optional = ["boto3", "cmaes (>=0.12.0)", "google-cloud-storage", "greenlet", "grpcio", "matplotlib (!=3.6.0)", "pandas", "plotly (>=4.9.0)", "protobuf (>=5.28.1)", "redis", "scikit-learn (>=0.24.2)", "scipy", "torch"] +test = ["fakeredis[lua]", "greenlet", "grpcio", "kaleido (<0.4)", "moto", "protobuf (>=5.28.1)", "pytest", "pytest-xdist", "scipy (>=1.9.2)", "torch"] + [[package]] name = "orjson" version = "3.11.7" @@ -3497,6 +3739,103 @@ files = [ {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.46" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ac245604295b521de49b465bab845e3afe6916bcb2147e5929c8041b4ec0545"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e6199143d51e3e1168bedd98cc698397404a8f7508831b81b6a29b18b051069"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:716be5bcabf327b6d5d265dbdc6213a01199be587224eb991ad0d37e83d728fd"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6f827fd687fa1ba7f51699e1132129eac8db8003695513fcf13fc587e1bd47a5"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c805fa6e5d461329fa02f53f88c914d189ea771b6821083937e79550bf31fc19"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-win32.whl", hash = "sha256:3aac08f7546179889c62b53b18ebf1148b10244b3405569c93984b0388d016a7"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-win_amd64.whl", hash = "sha256:0cc3117db526cad3e61074100bd2867b533e2c7dc1569e95c14089735d6fb4fe"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:90bde6c6b1827565a95fde597da001212ab436f1b2e0c2dcc7246e14db26e2a3"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b1e5f3a5f1ff4f42d5daab047428cd45a3380e51e191360a35cef71c9a7a2a"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93bb0aae40b52c57fd74ef9c6933c08c040ba98daf23ad33c3f9893494b8d3ce"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4e2cc868b7b5208aec6c960950b7bb821f82c2fe66446c92ee0a571765e91a5"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:965c62be8256d10c11f8907e7a8d3e18127a4c527a5919d85fa87fd9ecc2cfdc"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-win32.whl", hash = "sha256:9397b381dcee8a2d6b99447ae85ea2530dcac82ca494d1db877087a13e38926d"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl", hash = "sha256:4396c948d8217e83e2c202fbdcc0389cf8c93d2c1c5e60fa5c5a955eae0e64be"}, + {file = "sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e"}, + {file = "sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "sse-starlette" version = "3.2.0" @@ -4033,6 +4372,155 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "xxhash" +version = "3.6.0" +description = "Python binding for xxHash" +optional = false +python-versions = ">=3.7" +files = [ + {file = "xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71"}, + {file = "xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d"}, + {file = "xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8"}, + {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058"}, + {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2"}, + {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc"}, + {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc"}, + {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07"}, + {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4"}, + {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06"}, + {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4"}, + {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b"}, + {file = "xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b"}, + {file = "xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}, + {file = "xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}, + {file = "xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a"}, + {file = "xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa"}, + {file = "xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248"}, + {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62"}, + {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f"}, + {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e"}, + {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8"}, + {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0"}, + {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77"}, + {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c"}, + {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b"}, + {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3"}, + {file = "xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd"}, + {file = "xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef"}, + {file = "xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7"}, + {file = "xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c"}, + {file = "xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204"}, + {file = "xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490"}, + {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2"}, + {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa"}, + {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0"}, + {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2"}, + {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9"}, + {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e"}, + {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374"}, + {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d"}, + {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae"}, + {file = "xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb"}, + {file = "xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c"}, + {file = "xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829"}, + {file = "xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec"}, + {file = "xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1"}, + {file = "xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6"}, + {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263"}, + {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546"}, + {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89"}, + {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d"}, + {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7"}, + {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db"}, + {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42"}, + {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11"}, + {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd"}, + {file = "xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799"}, + {file = "xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392"}, + {file = "xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6"}, + {file = "xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702"}, + {file = "xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db"}, + {file = "xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54"}, + {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f"}, + {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5"}, + {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1"}, + {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee"}, + {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd"}, + {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729"}, + {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292"}, + {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf"}, + {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033"}, + {file = "xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec"}, + {file = "xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8"}, + {file = "xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746"}, + {file = "xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e"}, + {file = "xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405"}, + {file = "xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3"}, + {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6"}, + {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063"}, + {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7"}, + {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b"}, + {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd"}, + {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0"}, + {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152"}, + {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11"}, + {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5"}, + {file = "xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f"}, + {file = "xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad"}, + {file = "xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679"}, + {file = "xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4"}, + {file = "xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67"}, + {file = "xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad"}, + {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b"}, + {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b"}, + {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca"}, + {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a"}, + {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99"}, + {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3"}, + {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6"}, + {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93"}, + {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518"}, + {file = "xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119"}, + {file = "xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f"}, + {file = "xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95"}, + {file = "xxhash-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41"}, + {file = "xxhash-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8"}, + {file = "xxhash-3.6.0-cp38-cp38-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e"}, + {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850"}, + {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa"}, + {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788"}, + {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df"}, + {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1"}, + {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65"}, + {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87"}, + {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f"}, + {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b"}, + {file = "xxhash-3.6.0-cp38-cp38-win32.whl", hash = "sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536"}, + {file = "xxhash-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004"}, + {file = "xxhash-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a"}, + {file = "xxhash-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7"}, + {file = "xxhash-3.6.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2"}, + {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d"}, + {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec"}, + {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222"}, + {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919"}, + {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6"}, + {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360"}, + {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4"}, + {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86"}, + {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796"}, + {file = "xxhash-3.6.0-cp39-cp39-win32.whl", hash = "sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d"}, + {file = "xxhash-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802"}, + {file = "xxhash-3.6.0-cp39-cp39-win_arm64.whl", hash = "sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6"}, + {file = "xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0"}, + {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296"}, + {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13"}, + {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd"}, + {file = "xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d"}, + {file = "xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6"}, +] + [[package]] name = "yarl" version = "1.22.0" @@ -4199,4 +4687,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "b268e92061ffd1720bf8ed5252a00e6f0132e8d9e8df7b78ac3be3e492e26103" +content-hash = "9a3b7297f4254b101ea7184fad127421f243734e6923014c393615f87f7f7db6" diff --git a/pyproject.toml b/pyproject.toml index a1d54a2..b802600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.11,<3.14" -dspy = {version = "^3.3.0", extras = ["mcp"]} +dspy = {version = "^3.1.3", extras = ["mcp"]} litellm = "^1.81.6" cachetools = ">=5.0.0" PyGithub = ">=2.5.0" diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 777e5c6..e3b33e6 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -1,24 +1,15 @@ """DSPy and LiteLLM configuration utilities.""" -import asyncio import logging import dspy # type: ignore[import-untyped] from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] -from dspy.predict.react_v2 import ReActV2 as _ReActV2 # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] from codespy.config import Settings, get_settings from codespy.config_memory import LLMSettings, REFLECTION_MODULES -class AsyncReActV2(_ReActV2): - """ReActV2 with aforward() for async callers (acall).""" - - async def aforward(self, **kwargs): - return await asyncio.to_thread(self.forward, **kwargs) - - logger = logging.getLogger(__name__) @@ -162,7 +153,7 @@ def configure_dspy(settings: Settings) -> None: - Memory caching for LLM responses TwoStepAdapter decouples reasoning quality from format compliance, - solving ChatAdapter parsing failures with ReActV2 agents. + solving ChatAdapter parsing failures with ReAct agents. Args: settings: Application settings containing model and API key configuration. @@ -203,7 +194,7 @@ def configure_dspy(settings: Settings) -> None: dspy.settings.configure( lm=lm, - adapter=TwoStepAdapter(extraction_lm, use_native_function_calling=True), # TwoStepAdapter solves ChatAdapter parsing failures + adapter=TwoStepAdapter(extraction_lm), # TwoStepAdapter solves ChatAdapter parsing failures ) # Enable memory-only caching for LLM calls (no disk caching) diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 4357acc..b4bf238 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -139,6 +139,19 @@ def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: # Trajectory formatting with optional step-aware head+tail bounding # --------------------------------------------------------------------------- +def _format_step(i: int, entry: dict) -> str: + parts = [f"--- Step {i + 1} ---"] + if entry.get("reasoning"): + parts.append(f"Reasoning: {entry['reasoning']}") + code = entry.get("code", "") + if code: + parts.append(f"Code:\n{code}") + output = entry.get("output", "") + if output: + parts.append(f"Output:\n{output}") + return "\n".join(parts) + + # Tokens set aside for the "... (N tokens omitted) ..." marker so the returned # text honours max_tokens including the marker itself. _MARKER_RESERVE = 16 @@ -212,140 +225,108 @@ def _head_tail_text(text: str, max_tokens: int, head_ratio: float = 0.6) -> str: return "".join(head_lines) + marker + "".join(tail_lines) -def _format_step(i: int, entry: dict) -> str: - """Format a single step dict (CodeAct style) as readable text.""" - parts = [f"--- Step {i + 1} ---"] - if entry.get("reasoning"): - parts.append(f"Reasoning: {entry['reasoning']}") - code = entry.get("code", "") - if code: - parts.append(f"Code:\n{code}") - output = entry.get("output", "") - if output: - parts.append(f"Output:\n{output}") - return "\n".join(parts) - - -def _bound_steps(step_texts: list[str], max_tokens: int | None) -> str: - """Step-aware head/tail bounding for a list of formatted step strings. - - Keeps whole steps from front (60%) and back (40%), replaces middle with - omission marker. Caps oversized individual steps first. - """ - if max_tokens is None: - return "\n\n".join(step_texts) - - full = "\n\n".join(step_texts) - if count_tokens(full) <= max_tokens: - return full - - content_budget = max(max_tokens - _MARKER_RESERVE, 0) - head_budget = int(content_budget * 0.6) - tail_budget = content_budget - head_budget - - capped = [_head_tail_text(s, max_tokens) if count_tokens(s) > max_tokens else s for s in step_texts] - - head_steps: list[str] = [] - head_tokens = 0 - for s in capped: - t = count_tokens(s) - if head_tokens + t > head_budget: - break - head_steps.append(s) - head_tokens += t - - tail_steps: list[str] = [] - tail_tokens = 0 - for s in reversed(capped[len(head_steps):]): - t = count_tokens(s) - if tail_tokens + t > tail_budget: - break - tail_steps.append(s) - tail_tokens += t - tail_steps.reverse() - - if not head_steps and capped: - head_steps = [_head_tail_text(capped[0], head_budget)] - if not tail_steps and len(capped) > len(head_steps): - tail_steps = [_head_tail_text(capped[-1], tail_budget)] - - n_omitted = len(capped) - len(head_steps) - len(tail_steps) - parts = list(head_steps) - if n_omitted > 0: - first_omitted = len(head_steps) + 1 - last_omitted = len(capped) - len(tail_steps) - parts.append(f"--- Steps {first_omitted}–{last_omitted} omitted ({n_omitted} steps) ---") - parts.extend(tail_steps) - return "\n\n".join(parts) - - -def _format_list_traj(traj: list[dict], max_tokens: int | None) -> str: - """Format a CodeAct list trajectory with step-aware bounding.""" - step_texts = [_format_step(i, entry) for i, entry in enumerate(traj)] - return _bound_steps(step_texts, max_tokens) - - -def _format_history_event(i: int, event: dict) -> str: - """Format one ReActV2 history turn as readable text.""" - parts = [f"--- Turn {i + 1} ---"] - if "next_thought" in event: - parts.append(f"Thought: {event['next_thought']}") - if "tool_calls" in event: - tc = event["tool_calls"] - if hasattr(tc, "tool_calls"): - for call in tc.tool_calls: - call_str = f"Tool: {call.name}({call.args or {}})" - if hasattr(tc, "tool_call_results") and tc.tool_call_results: - results = getattr(tc.tool_call_results, "tool_call_results", []) or [] - matching = [r for r in results if getattr(r, "call_id", None) == call.id] - if matching: - call_str += f"\n -> {matching[0].value}" - parts.append(call_str) - else: - parts.append(f"ToolCalls: {tc}") - for k, v in event.items(): - if k not in ("next_thought", "tool_calls"): - parts.append(f"{k}: {v}") - return "\n".join(parts) - - -def _format_history(history, max_tokens: int | None) -> str: - """Format a dspy.History (ReActV2) with step-aware bounding.""" - messages = history.messages if hasattr(history, "messages") else [] - step_texts = [ - _format_history_event(i, ev) if isinstance(ev, dict) else f"--- Turn {i + 1} ---\n{ev}" - for i, ev in enumerate(messages) - ] - return _bound_steps(step_texts, max_tokens) - - def format_trajectory(pred: dspy.Prediction, max_tokens: int | None = None) -> str: - """Serialize a prediction's execution trace to bounded text. - - Dispatches by prediction shape: - history (dspy.History) - ReActV2: structured turn messages - trajectory (list) - CodeAct: list of step dicts - trajectory (dict) - legacy ReAct: flat key/value dump - fallback - pred.toDict() or str(pred) + """Serialize a dspy trajectory to text, with optional head+tail bounding. + + Args: + pred: The dspy Prediction returned by the wrapped agent. + max_tokens: If None (default), the full trajectory is returned so the + Distiller can do all compression. If set, step-aware head+tail + bounding is applied: whole steps are kept from both the front and + back of the trajectory (60 % head / 40 % tail), and the middle is + replaced by an omission marker. For dict / fallback trajectories, + the same head+tail logic is applied at line granularity. A single + oversized step's Output block is itself head+tail bounded before + the per-step budget accounting. + + Trajectory shapes handled: + list — ReAct / CodeAct: list of dicts with 'code', 'output', + optional 'reasoning'. Step-aware bounding. + dict — flat key/value dump. Line-granularity bounding. + other — str(pred) or pred.toDict() fallback. Line-granularity bounding. """ - # ReActV2: history attribute - history = getattr(pred, "history", None) - if history is not None and hasattr(history, "messages"): - return _format_history(history, max_tokens) - - # Legacy: trajectory attribute traj = getattr(pred, "trajectory", None) + # ----- list path (ReAct / CodeAct) ----- if isinstance(traj, list): - return _format_list_traj(traj, max_tokens) + step_texts = [_format_step(i, entry) for i, entry in enumerate(traj)] + + if max_tokens is None: + return "\n\n".join(step_texts) + + # Check if everything fits as-is + full = "\n\n".join(step_texts) + if count_tokens(full) <= max_tokens: + return full + + content_budget = max(max_tokens - _MARKER_RESERVE, 0) + head_budget = int(content_budget * 0.6) + tail_budget = content_budget - head_budget + + # Cap individual oversized step outputs before budgeting + capped: list[str] = [] + for s in step_texts: + if count_tokens(s) > max_tokens: + s = _head_tail_text(s, max_tokens) + capped.append(s) + + # Greedily keep head steps + head_steps: list[str] = [] + head_tokens = 0 + for s in capped: + t = count_tokens(s) + if head_tokens + t > head_budget: + break + head_steps.append(s) + head_tokens += t + + # Greedily keep tail steps (from the end), never reusing a head step + tail_steps: list[str] = [] + tail_tokens = 0 + for s in reversed(capped[len(head_steps):]): + t = count_tokens(s) + if tail_tokens + t > tail_budget: + break + tail_steps.append(s) + tail_tokens += t + tail_steps.reverse() + + # If no whole step fits in either half (every step is larger than its + # budget), fall back to bounding single steps so the budget is actually + # used instead of returning just the omission marker. + if not head_steps and capped: + head_steps = [_head_tail_text(capped[0], head_budget)] + if not tail_steps and len(capped) > len(head_steps): + tail_steps = [_head_tail_text(capped[-1], tail_budget)] + + # Determine omitted range + n_head = len(head_steps) + n_tail = len(tail_steps) + n_total = len(capped) + n_omitted = n_total - n_head - n_tail + + parts = list(head_steps) + if n_omitted > 0: + first_omitted = n_head + 1 + last_omitted = n_total - n_tail + parts.append( + f"--- Steps {first_omitted}–{last_omitted} omitted ({n_omitted} steps) ---" + ) + parts.extend(tail_steps) + return "\n\n".join(parts) + # ----- dict path ----- if isinstance(traj, dict): text = "\n".join(f"{k}: {v}" for k, v in traj.items()) - return text if max_tokens is None else _head_tail_text(text, max_tokens) + if max_tokens is None: + return text + return _head_tail_text(text, max_tokens) - # Fallback + # ----- fallback ----- try: text = "\n".join(f"{k}: {v}" for k, v in pred.toDict().items()) except Exception: text = str(pred) - return text if max_tokens is None else _head_tail_text(text, max_tokens) + if max_tokens is None: + return text + return _head_tail_text(text, max_tokens) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index b8ac785..7d328de 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -118,7 +118,7 @@ def __init__( ): """ Args: - module: Any dspy.Module (ReActV2, RLM, Predict, …) to wrap. + module: Any dspy.Module (ReAct, RLM, Predict, …) to wrap. budget: The four token budgets bounding memory, as a :class:`MemoryBudget`. Defaults to ``MemoryBudget()`` — see that class for per-field guidance. Resolve one from configuration with @@ -134,14 +134,14 @@ class for per-field guidance. Resolve one from configuration with If None, all input fields are serialized (bounded by ``budget.max_question_tokens``). Set this when one field cleanly captures user intent. - task_name: Identity recorded in ``Episode.task`` and used in the episode - filename. Pass the signature's snake_case name (``"doc"``, - ``"code_review"``, …) — the same key that drives config, LM - selection and cost attribution — so the episode path lines up with - the rest of the system. Inference is a last resort: only - ``dspy.ReActV2``-style modules expose ``.signature``, - ``dspy.ChainOfThought`` does not, so the fallback would yield a - meaningless (and collision-prone) ``"ChainOfThought"``. + task_name: Identity recorded in ``Episode.task`` and used in the episode + filename. Pass the signature's snake_case name (``"doc"``, + ``"code_review"``, …) — the same key that drives config, LM + selection and cost attribution — so the episode path lines up with + the rest of the system. Inference is a last resort: only + ``dspy.ReAct``-style modules expose ``.signature``, + ``dspy.ChainOfThought`` does not, so the fallback would yield a + meaningless (and collision-prone) ``"ChainOfThought"``. run_id: Identifier of the pipeline run this agent belongs to. Passed down by the orchestrating ``ReviewPipeline`` so every module invoked within the same review run shares the same identifier, @@ -163,15 +163,12 @@ class for per-field guidance. Resolve one from configuration with module_inputs = set(top_sig.input_fields) module.signature = prepend_context_map(top_sig) for _, pred in module.named_predictors(): - pred_sig = getattr(pred, "signature", None) - if pred_sig is not None and set(pred_sig.input_fields) & module_inputs: - if "context_map" not in pred_sig.input_fields: - pred.signature = prepend_context_map(pred_sig) + if set(pred.signature.input_fields) & module_inputs: + if "context_map" not in pred.signature.input_fields: + pred.signature = prepend_context_map(pred.signature) else: for _, pred in module.named_predictors(): - pred_sig = getattr(pred, "signature", None) - if pred_sig is not None: - pred.signature = prepend_context_map(pred_sig) + pred.signature = prepend_context_map(pred.signature) self.agent = module self.distill = Distiller() diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 5d5d532..4d46206 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,7 +8,6 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult @@ -229,7 +228,7 @@ async def aforward( scope_root = resolve_scope_root(repo_path, scope.subroot) tools, contexts = await self._create_tools(scope_root) try: - agent = AsyncReActV2( + agent = dspy.ReAct( signature=CodeReviewSignature, tools=tools, max_iters=max_iters, diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index aedc263..8ad5a96 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -1,6 +1,6 @@ -"""Scope resolver module - merged deterministic analysis + ReActV2 agent refinement. +"""Scope resolver module - merged deterministic analysis + ReAct agent refinement. -This module combines deterministic scope identification with a ReActV2 agent +This module combines deterministic scope identification with a ReAct agent for intelligent refinement. The agent uses filesystem and search tools to explore the codebase and make informed scope decisions, replacing the previous ChainOfThought predictor that relied on a static repo tree. @@ -19,7 +19,6 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import ContextMap, Hippocampus from codespy.agents.reviewer.models import ( PackageManifest, @@ -364,7 +363,7 @@ def derive_sparse_paths(changed_files: list[str]) -> list[str]: # For glob patterns like *.csproj, we need to add the pattern itself paths.append(manifest_pattern) - # Agent config directories — project instructions for ReActV2 agents + # Agent config directories — project instructions for ReAct agents paths.extend([ ".claude/", ".kilo/", @@ -879,7 +878,7 @@ async def _refine_scopes( review_context: ReviewContext | None, run_id: str | None, ) -> list[ScopeResult]: - """Use ReActV2 agent to refine scope assignments from deterministic candidates. + """Use ReAct agent to refine scope assignments from deterministic candidates. Args: scopes: Already-resolved scope results @@ -901,7 +900,7 @@ async def _refine_scopes( max_iters = self._settings.get_max_iters("scope") tools, contexts = await self._create_tools(repo_path) try: - agent = AsyncReActV2( + agent = dspy.ReAct( signature=ScopeRefinementSignature, tools=tools, max_iters=max_iters, diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index ee4ffec..fd7ba22 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,7 +8,6 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.dspy_config import AsyncReActV2 from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.memory.hippocampus import ContextMap from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult @@ -314,7 +313,7 @@ async def aforward( try: # Combine scoped filesystem tools with shared OSV tools all_tools = scoped_tools + osv_tools - supply_chain_agent = AsyncReActV2( + supply_chain_agent = dspy.ReAct( signature=SupplyChainSecuritySignature, tools=all_tools, max_iters=supply_chain_max_iters, diff --git a/src/codespy/config.py b/src/codespy/config.py index 3fad410..0e88a2f 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -265,7 +265,7 @@ def get_memory_max_context_map_tokens(self, signature_name: str) -> int: Bounds the rendered ContextMap — the persisted artifact that is prepended to every predictor of the wrapped agent, and therefore re-sent on every - ReActV2 iteration. + ReAct iteration. """ config = self.get_signature_config(signature_name).memory return ( diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index e7cbfb8..4606693 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -95,7 +95,7 @@ class MemoryConfig(BaseModel): # Ceiling on the rendered ContextMap. This is the *persisted* artifact and it # is prepended to every predictor of the wrapped agent, so it is re-sent on - # every ReActV2 iteration (~default_max_iters times per scope) plus once per + # every ReAct iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. # Approximate item capacity is default_max_context_map_tokens divided by # default_max_context_item_tokens (3072 / 240 ~= 12 items). From 6c251501cb10594bc0037db6317967973d4d6a3e Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Wed, 12 Aug 2026 09:36:10 +0200 Subject: [PATCH 58/79] wip --- src/codespy/agents/cost_tracker.py | 8 +++ .../agents/memory/hippocampus/hippocampus.py | 27 ++++++--- .../agents/reviewer/modules/scope_resolver.py | 7 ++- src/codespy/config_memory.py | 18 +----- .../tools/storage/filesystem/server.py | 7 ++- src/codespy/tools/storage/s3/client.py | 21 +++++-- src/codespy/tools/storage/s3/server.py | 7 ++- tests/test_s3_client.py | 55 +++++++++++++------ 8 files changed, 98 insertions(+), 52 deletions(-) diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index c1bc21b..b364141 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -4,6 +4,7 @@ even during parallel execution with dspy.Parallel. """ +import logging import sys import threading import time @@ -14,6 +15,8 @@ import dspy # type: ignore[import-untyped] +logger = logging.getLogger(__name__) + @dataclass class SignatureStats: @@ -290,6 +293,9 @@ def __exit__( ) -> None: """Exit the context, calculating costs from new history entries. + Cost calculation failures are logged at WARNING level but never + propagated — bookkeeping must not mask application errors. + The LM context is released in a ``finally``: a leaked ``dspy.context`` does not raise, it silently leaves the overridden LM installed for the remainder of the thread, so every later predictor @@ -303,6 +309,8 @@ def __exit__( entries = _get_history_entries() cost, tokens, call_count = _calculate_costs_from_entries(entries, self._before_uuids) self.tracker.end_signature(self.signature_name, cost, tokens, call_count) + except Exception as e: + logger.warning("Cost calculation failed for %s: %s", self.signature_name, e) finally: if self._lm_context is not None: self._lm_context.__exit__(exc_type, exc_val, exc_tb) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 7d328de..0f4569c 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -190,9 +190,19 @@ class for per-field guidance. Resolve one from configuration with # Identity of the wrapped module/signature for Episode metadata. An explicit # task_name wins: inference only works for modules exposing .signature. - self._task_name: str = task_name or ( - top_sig.__name__ if top_sig is not None else type(module).__name__ - ) + self._task_name: str + if task_name: + self._task_name = task_name + elif top_sig is not None: + self._task_name = top_sig.__name__ + else: + fallback = type(module).__name__ + logger.warning( + "Hippocampus: task_name not provided and module %r has no .signature; " + "using collision-prone fallback %r. Pass task_name explicitly.", + module, fallback, + ) + self._task_name = fallback self._module_name: str = type(module).__name__ # Identifier of the pipeline run this agent belongs to (see run_id arg # above). Falls back to a random UUID for standalone usage where no @@ -258,7 +268,7 @@ def _consolidate(self) -> str | None: Returns the combined trajectory text used for consolidation, or ``None`` if the buffer is empty (no-op). """ - skip_double_distill = len(self._episode_trajectories)==1 and self._reflected_count>0 + skip_double_distill = len(self._episode_trajectories) == 1 and self._reflected_count > 0 if not self._episode_trajectories or skip_double_distill: return None combined = "\n\n".join( @@ -356,8 +366,9 @@ def end_episode( Raises: OSError: If persistence is requested and the write fails. """ - nothing_to_persist = self._consolidate() is None and self._reflected_count==0 - if nothing_to_persist: + combined = self._consolidate() + has_content = combined is not None or self._reflected_count > 0 + if not has_content: return self._finalize_episode(artifacts) if store is not None and dir is not None: @@ -385,8 +396,8 @@ async def aend_episode( episode (e.g. ``{"review": ""}``). """ combined = await asyncio.to_thread(self._consolidate) - nothing_to_persist = combined is None and self._reflected_count == 0 - if nothing_to_persist: + has_content = combined is not None or self._reflected_count > 0 + if not has_content: return await asyncio.to_thread(self._finalize_episode, artifacts) if store is not None and dir is not None: diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 8ad5a96..6805bcb 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -508,14 +508,19 @@ async def _ensure_manifests(self, repo_path: Path, changed_files: list[str]) -> # Checkout missing manifests try: + from git import Repo + from git.exc import GitCommandError + repo = Repo(repo_path) for path in manifest_paths: if not (repo_path / path).exists(): try: repo.git.checkout("HEAD", "--", path) logger.debug("Checked out manifest: %s", path) - except Exception: + except GitCommandError: pass # File doesn't exist in repo — expected + except Exception as e: + logger.warning("Unexpected error checking out manifest %s: %s", path, e) except Exception as e: logger.warning("Failed to ensure manifests: %s", e) diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 4606693..df5b38b 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -2,13 +2,10 @@ from __future__ import annotations -import logging import os from typing import TYPE_CHECKING, Any, Literal -from pydantic import BaseModel, Field, model_validator - -logger = logging.getLogger(__name__) +from pydantic import BaseModel, Field from codespy.config_dspy import ReasoningEffort from codespy.tools.storage.base import Storage @@ -57,19 +54,6 @@ class LLMSettings(BaseModel): # ``new_lm`` clamps this to the model's real output ceiling before use. max_tokens: int - @model_validator(mode="after") - def _enforce_temperature_with_reasoning(self) -> "LLMSettings": - """Providers require temperature=1 when reasoning is enabled.""" - if self.reasoning_effort is not None and self.temperature != 1.0: - logger.warning( - "temperature=%.2f is incompatible with reasoning_effort=%r; " - "forcing temperature=1.0", - self.temperature, - self.reasoning_effort, - ) - self.temperature = 1.0 - return self - class MemoryConfig(BaseModel): diff --git a/src/codespy/tools/storage/filesystem/server.py b/src/codespy/tools/storage/filesystem/server.py index 8ff53c5..60b5f75 100644 --- a/src/codespy/tools/storage/filesystem/server.py +++ b/src/codespy/tools/storage/filesystem/server.py @@ -3,6 +3,7 @@ import logging import os import sys +from collections import OrderedDict from functools import lru_cache from pathlib import Path @@ -19,7 +20,8 @@ _fs: FileSystem | None = None # Manual cache for read_file to skip caching error results -_read_file_cache: dict[tuple[str, int, int | None], tuple] = {} +_READ_FILE_CACHE_MAX = 512 +_read_file_cache: OrderedDict[tuple[str, int, int | None], tuple] = OrderedDict() def _get_fs() -> FileSystem: @@ -33,11 +35,14 @@ def _read_file_cached(path: str, max_bytes: int, max_lines: int | None) -> tuple """Cached version of read_file that doesn't cache error results.""" key = (path, max_bytes, max_lines) if key in _read_file_cache: + _read_file_cache.move_to_end(key) return _read_file_cache[key] result = _get_fs().read_file(path, max_bytes, max_lines) dumped = tuple(sorted(result.model_dump().items())) if not result.error: _read_file_cache[key] = dumped + if len(_read_file_cache) > _READ_FILE_CACHE_MAX: + _read_file_cache.popitem(last=False) return dumped diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index 6fe3322..4752d96 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -61,9 +61,14 @@ def __init__( # ------------------------------------------------------------------ def _resolve_path(self, path: str) -> str: - normalised = posixpath.normpath(path.strip("/")) + stripped = path.strip("/") + # Reject '..' in any path component before normalization + if any(part == ".." for part in stripped.split("/")): + raise ValueError(f"Path escapes bucket root: {path!r}") + normalised = posixpath.normpath(stripped) if normalised == ".": return "" + # Belt-and-suspenders: catch anything normpath might produce if normalised.startswith(".."): raise ValueError(f"Path escapes bucket root: {path!r}") return normalised @@ -246,11 +251,15 @@ def read_file( if len(raw) > max_bytes: truncated = True raw = raw[:max_bytes] - # Back up to a valid UTF-8 character boundary - while raw and (raw[-1] & 0xC0) == 0x80: - raw = raw[:-1] - if raw and raw[-1] >= 0xC0: - raw = raw[:-1] + # Ensure truncation didn't split a multi-byte UTF-8 sequence. + # Only the last 1-3 bytes can be an incomplete character. + try: + raw.decode("utf-8") + except UnicodeDecodeError as e: + # Only trim if the error is at the truncation boundary (last 4 bytes) + if e.start >= len(raw) - 4: + raw = raw[:e.start] + # Interior errors are handled by the full decode below (latin-1 fallback) try: content = raw.decode("utf-8") diff --git a/src/codespy/tools/storage/s3/server.py b/src/codespy/tools/storage/s3/server.py index 07f6ec3..dc71e47 100644 --- a/src/codespy/tools/storage/s3/server.py +++ b/src/codespy/tools/storage/s3/server.py @@ -3,6 +3,7 @@ import logging import os import sys +from collections import OrderedDict from functools import lru_cache from mcp.server.fastmcp import FastMCP @@ -18,7 +19,8 @@ _client: S3Client | None = None # Manual cache for read_file to skip caching error results -_read_file_cache: dict[tuple[str, int, int | None], tuple] = {} +_READ_FILE_CACHE_MAX = 512 +_read_file_cache: OrderedDict[tuple[str, int, int | None], tuple] = OrderedDict() def _get_client() -> S3Client: @@ -128,11 +130,14 @@ def _read_file_cached(path: str, max_bytes: int, max_lines: int | None) -> tuple """Cached version of read_file that doesn't cache error results.""" key = (path, max_bytes, max_lines) if key in _read_file_cache: + _read_file_cache.move_to_end(key) return _read_file_cache[key] result = _get_client().read_file(path, max_bytes, max_lines) dumped = tuple(sorted(result.model_dump().items())) if not result.error: _read_file_cache[key] = dumped + if len(_read_file_cache) > _READ_FILE_CACHE_MAX: + _read_file_cache.popitem(last=False) return dumped diff --git a/tests/test_s3_client.py b/tests/test_s3_client.py index 7fee1be..ebdfdc7 100644 --- a/tests/test_s3_client.py +++ b/tests/test_s3_client.py @@ -27,32 +27,51 @@ def test_rejects_leading_traversal(self): with pytest.raises(ValueError, match="escapes bucket root"): self.client._resolve_path("../secret") + def test_rejects_interior_traversal(self): + """Paths with '..' anywhere are now rejected, not just those escaping root.""" + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("a/b/../c") + def test_empty_after_normalization(self): assert self.client._resolve_path(".") == "" assert self.client._resolve_path("/") == "" class TestReadFileTruncation: + @staticmethod + def _truncate_utf8(raw: bytes, max_bytes: int) -> bytes: + """Reproduce the fixed truncation logic.""" + if len(raw) <= max_bytes: + return raw + raw = raw[:max_bytes] + try: + raw.decode("utf-8") + except UnicodeDecodeError as e: + if e.start >= len(raw) - 4: + raw = raw[:e.start] + return raw + def test_truncate_preserves_utf8(self): # "café" = 63 61 66 c3 a9 (5 bytes), max_bytes=4 cuts inside é - raw = "café".encode("utf-8") # 5 bytes - max_bytes = 4 - # Simulate truncation logic - truncated_raw = raw[:max_bytes] # b'caf\xc3' — incomplete é - while truncated_raw and (truncated_raw[-1] & 0xC0) == 0x80: - truncated_raw = truncated_raw[:-1] - if truncated_raw and truncated_raw[-1] >= 0xC0: - truncated_raw = truncated_raw[:-1] - result = truncated_raw.decode("utf-8") - assert result == "caf" # Clean cut before multi-byte char + raw = "café".encode("utf-8") + result = self._truncate_utf8(raw, 4).decode("utf-8") + assert result == "caf" def test_truncate_emoji(self): - raw = "hi🎉bye".encode("utf-8") # "hi" (2) + 🎉 (4) + "bye" (3) = 9 bytes - max_bytes = 4 - truncated_raw = raw[:max_bytes] # b'hi\xf0\x9f' — incomplete emoji - while truncated_raw and (truncated_raw[-1] & 0xC0) == 0x80: - truncated_raw = truncated_raw[:-1] - if truncated_raw and truncated_raw[-1] >= 0xC0: - truncated_raw = truncated_raw[:-1] - result = truncated_raw.decode("utf-8") + raw = "hi🎉bye".encode("utf-8") # 9 bytes + result = self._truncate_utf8(raw, 4).decode("utf-8") assert result == "hi" + + def test_truncate_at_exact_boundary_preserves_character(self): + """Bug regression: truncation at valid char boundary must NOT strip it.""" + # "àè" = c3 a0 c3 a8 (4 bytes), max_bytes=4 lands exactly at end of è + raw = "àè".encode("utf-8") + result = self._truncate_utf8(raw, 4).decode("utf-8") + assert result == "àè" # Both characters preserved (was bug: stripped è) + + def test_truncate_between_two_multibyte(self): + """Truncation between two multi-byte characters preserves the first.""" + # "àè" = c3 a0 c3 a8, max_bytes=3 cuts inside è + raw = "àè".encode("utf-8") + result = self._truncate_utf8(raw, 3).decode("utf-8") + assert result == "à" # è is incomplete, stripped From 95a2cd38c558890f9573a1901815dfed298fab12 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 16 Aug 2026 10:49:33 +0200 Subject: [PATCH 59/79] wip --- .env.example | 30 +- codespy.yaml | 30 +- .../agents/memory/hippocampus/__init__.py | 12 +- .../agents/memory/hippocampus/budget.py | 32 +- .../agents/memory/hippocampus/context_map.py | 222 -------- .../memory/hippocampus/context_memory.py | 418 +++++++++++++++ .../agents/memory/hippocampus/episode.py | 29 +- .../agents/memory/hippocampus/hippocampus.py | 99 ++-- .../memory/hippocampus/modules/__init__.py | 2 +- .../hippocampus/modules/cartographer.py | 26 +- .../memory/hippocampus/modules/distiller.py | 22 +- src/codespy/agents/reviewer/models.py | 27 +- .../agents/reviewer/modules/auditor.py | 5 +- .../agents/reviewer/modules/code_reviewer.py | 31 +- .../agents/reviewer/modules/doc_reviewer.py | 30 +- .../agents/reviewer/modules/helpers.py | 2 +- .../reviewer/modules/manifest_parser.py | 307 +++++++++++ .../agents/reviewer/modules/scope_resolver.py | 107 +++- .../agents/reviewer/modules/summarizer.py | 18 +- .../reviewer/modules/supply_chain_auditor.py | 32 +- src/codespy/agents/reviewer/reviewer.py | 50 +- src/codespy/config.py | 20 +- src/codespy/config_dspy.py | 4 +- src/codespy/config_memory.py | 16 +- tests/test_context_memory.py | 480 ++++++++++++++++++ 25 files changed, 1599 insertions(+), 452 deletions(-) delete mode 100644 src/codespy/agents/memory/hippocampus/context_map.py create mode 100644 src/codespy/agents/memory/hippocampus/context_memory.py create mode 100644 src/codespy/agents/reviewer/modules/manifest_parser.py create mode 100644 tests/test_context_memory.py diff --git a/.env.example b/.env.example index 2fbee45..edd78a1 100644 --- a/.env.example +++ b/.env.example @@ -104,7 +104,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # # Cheap (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory -# reflection — summarizing a trajectory and curating the context map. +# reflection — summarizing a trajectory and curating the context memory. # Compact, frequent tasks. # Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # @@ -156,7 +156,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Memory (Hippocampus) # ============================================================================= # Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# consolidate their run into a ContextMap and persist it as an Episode. +# consolidate their run into a ContextMemory and persist it as an Episode. # Save-only for now (no loading). Disabled by default per-signature — see # the per-signature MEMORY_* settings below. # @@ -184,13 +184,13 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # # Four independent token budgets, most to least cost-sensitive: # -# 1. Ceiling on the rendered ContextMap. This is the persisted artifact, and it is +# 1. Ceiling on the rendered ContextMemory. This is the persisted artifact, and it is # prepended to every agent iteration, so it is re-sent ~DEFAULT_MAX_ITERS times -# per scope. Divided by MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS it gives the map's item +# per scope. Divided by MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS it gives the memory's item # capacity (3072 / 240 ~= 12 items). -# MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=3072 -# 2. Budget for a SINGLE context-map item, given to the Distiller and the -# Cartographer as a prompt input so no one item eats the whole map. Soft limit +# MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS=3072 +# 2. Budget for a SINGLE context-memory item, given to the Distiller and the +# Cartographer as a prompt input so no one item eats the whole memory. Soft limit # (expressed to the LLM, not enforced — truncating an item could corrupt an exact # constant). Lower it for more, terser items; raise it for fewer, richer ones. # MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS=240 @@ -205,7 +205,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS=2048 # LLM settings for the reflection modules. The Distiller summarizes a trajectory; -# the Cartographer curates the context map. Both are compact summarize/curate +# the Cartographer curates the context memory. Both are compact summarize/curate # tasks, so a cheaper tier than code review usually suffices. # Each falls back to the corresponding DEFAULT_* value when unset. # MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 @@ -257,8 +257,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - MAX_TOKENS (integer) - Output token budget (unset -> DEFAULT_MAX_TOKENS) # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) -# - MEMORY_MAX_CONTEXT_MAP_TOKENS (integer) - Ceiling on the persisted ContextMap -# - MEMORY_MAX_CONTEXT_ITEM_TOKENS (integer) - Budget for a single context-map item +# - MEMORY_MAX_CONTEXT_MEMORY_TOKENS (integer) - Ceiling on the persisted ContextMemory +# - MEMORY_MAX_CONTEXT_ITEM_TOKENS (integer) - Budget for a single context-memory item # - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection # - MEMORY_MAX_QUESTION_TOKENS (integer) - Cap on serialized inputs used as the question @@ -272,7 +272,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 # CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -283,7 +283,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_SCAN_UNCHANGED=false # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -292,7 +292,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 -# DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 # DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # DOC_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -303,7 +303,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SCOPE_MAX_TOKENS=64000 # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 -# SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 # SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # Unused: scope uses question_field="mr_title", so no inputs are serialized. @@ -313,7 +313,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # SUMMARY_MEMORY_ENABLED=true # SUMMARY_MEMORY_MAX_REFLECTS=1 -# SUMMARY_MEMORY_MAX_CONTEXT_MAP_TOKENS=3072 +# SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 # SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUMMARY_MEMORY_MAX_QUESTION_TOKENS=2048 diff --git a/codespy.yaml b/codespy.yaml index 1eae78b..3249fbf 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -61,7 +61,7 @@ gitlab: # MEMORY # ============================================================================ # Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# and the summary step consolidate their run into a ContextMap and +# and the summary step consolidate their run into a ContextMemory and # persist it as an Episode. Save-only for now (no loading). Disabled by # default globally; enabled by default for summary — see `memory:` # blocks under each signature below. @@ -87,12 +87,12 @@ memory: # Four independent token budgets, from most to least cost-sensitive: # - # 1. max_context_map_tokens — ceiling on the rendered ContextMap. This is the + # 1. max_context_memory_tokens — ceiling on the rendered ContextMemory. This is the # persisted artifact, and it is prepended to every agent iteration, so it is # re-sent ~default_max_iters times per scope. Divided by max_context_item_tokens it - # gives the map's item capacity (3072 / 240 ~= 12 items). - # 2. max_context_item_tokens — budget for a SINGLE context-map item, given to the Distiller - # and the Cartographer as a prompt input so no one item eats the whole map. + # gives the memory's item capacity (3072 / 240 ~= 12 items). + # 2. max_context_item_tokens — budget for a SINGLE context memory item, given to the Distiller + # and the Cartographer as a prompt input so no one item eats the whole memory. # Soft limit (expressed to the LLM, not enforced — truncating an item could # corrupt an exact constant). Lower it for more, terser items; raise it for # fewer, richer ones. @@ -103,13 +103,13 @@ memory: # 4. max_question_tokens — cap on the serialized agent inputs used as the reflection # "question". Without it, every input field is sent in full (for code review that # means the complete patch of every changed file). null = unbounded. - default_max_context_map_tokens: 3072 # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS + default_max_context_memory_tokens: 3072 # MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS default_max_context_item_tokens: 240 # MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS default_max_trajectory_tokens: 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS default_max_question_tokens: 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS # LLM settings for the reflection modules. The Distiller summarizes a - # trajectory; the Cartographer curates the context map. Both are compact + # trajectory; the Cartographer curates the context memory. Both are compact # summarize/curate tasks, so a cheaper tier than code review usually suffices. # null inherits the top-level default_* value. distiller: @@ -147,9 +147,9 @@ memory: # Cheap (summary): Used for PR summary generation. Simple synthesis # task. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # -# Cheap (memory.distiller / memory.cartographer): Memory reflection — -# summarizing a trajectory and curating the context map. Compact, frequent -# tasks. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. + # Cheap (memory.distiller / memory.cartographer): Memory reflection — + # summarizing a trajectory and curating the context memory. Compact, frequent + # tasks. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # # By default, all models fall back to default_model. Override extraction_model, # the summary model, and the reflection models for cost optimization: @@ -205,7 +205,7 @@ signatures: memory: enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS @@ -222,7 +222,7 @@ signatures: memory: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS @@ -238,7 +238,7 @@ signatures: memory: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # DOC_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # DOC_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: null # DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: null # DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # DOC_MEMORY_MAX_QUESTION_TOKENS @@ -254,7 +254,7 @@ signatures: memory: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS # Unused: scope passes question_field="mr_title", so no inputs are serialized. @@ -270,7 +270,7 @@ signatures: memory: enabled: true # SUMMARY_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUMMARY_MEMORY_MAX_REFLECTS - max_context_map_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: null # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUMMARY_MEMORY_MAX_QUESTION_TOKENS diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index 7e4ce45..a56e4f6 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -1,13 +1,16 @@ from codespy.agents.memory.hippocampus.budget import MemoryBudget -from codespy.agents.memory.hippocampus.context_map import ( +from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, - ContextMap, + ContextMemory, Item, ItemTag, Mutation, Operation, OpType, SectionName, + Topic, + compute_common_ancestor_topic_id, + make_topic_id, ) from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.hippocampus.hippocampus import Hippocampus @@ -18,7 +21,7 @@ "CacheCandidate", "Cartographer", "CartographerSig", - "ContextMap", + "ContextMemory", "Distiller", "DistillerSig", "Episode", @@ -30,4 +33,7 @@ "Operation", "OpType", "SectionName", + "Topic", + "compute_common_ancestor_topic_id", + "make_topic_id", ] diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index b4bf238..debfe9c 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -5,7 +5,7 @@ import dspy import tiktoken -from codespy.agents.memory.hippocampus.context_map import ContextMap +from codespy.agents.memory.hippocampus.context_memory import ContextMemory _ENCODING = tiktoken.get_encoding("o200k_base") @@ -25,20 +25,20 @@ class MemoryBudget: once (see ``Settings.get_memory_budget``) and shared across instances. Attributes: - max_context_map_tokens: Hard ceiling on the rendered ContextMap, + max_context_memory_tokens: Hard ceiling on the rendered ContextMemory, enforced by the Evictor after every reflection. This is the *persisted* artifact and it is prepended to every predictor of the wrapped agent, so it is re-sent on every agent iteration (~``max_iters`` times per run) plus once per reflection call — the most cost-sensitive of the four. Divided by ``max_context_item_tokens`` it - gives the map's approximate item capacity (3072 / 240 ~= 12 items). - max_context_item_tokens: Budget for a *single* context-map item, passed to the + gives the memory's approximate item capacity (3072 / 240 ~= 12 items). + max_context_item_tokens: Budget for a *single* context memory item, passed to the Distiller and the Cartographer as a prompt input so they keep each - item compact rather than spending the whole map budget on one + item compact rather than spending the whole memory budget on one verbose entry. Unlike the other three this is a **soft** budget: expressed to the LLM, not enforced in code, since truncating an item could corrupt an exact constant it holds. Lower it to fit - more, terser items in the same map budget; raise it for richer + more, terser items in the same memory budget; raise it for richer items. max_trajectory_tokens: Budget for trajectories fed to the Distiller. None = full trajectory, Distiller does all compression — only @@ -59,7 +59,7 @@ class MemoryBudget: field cleanly captures intent. """ - max_context_map_tokens: int = 3072 + max_context_memory_tokens: int = 3072 max_context_item_tokens: int = 240 max_trajectory_tokens: int | None = 8192 max_question_tokens: int | None = 2048 @@ -88,7 +88,7 @@ def count_tokens(s: str) -> int: # --------------------------------------------------------------------------- def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: - """Serialize call inputs (excluding context_map) for the Distiller question. + """Serialize call inputs (excluding context_memory) for the Distiller question. All fields are included in full. If max_tokens is set, the joined result is head+tail bounded via _head_tail_text so both the instruction and any @@ -97,7 +97,7 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: """ parts: list[str] = [] for k, v in kwargs.items(): - if k == "context_map": + if k == "context_memory": continue parts.append(f"{k}: {v}") text = "\n".join(parts) @@ -110,13 +110,13 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: # Eviction # --------------------------------------------------------------------------- -def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: - if count_tokens(cmap.render()) <= budget: - return cmap +def evict(context_memory: ContextMemory, scores: dict[str, int], budget: int) -> ContextMemory: + if count_tokens(context_memory.render()) <= budget: + return context_memory item_section: dict[str, str] = { - it.id: sec for sec in cmap.section_names() for it in cmap.section(sec) + it.id: sec for sec in context_memory.section_names() for it in context_memory.section(sec) } - flat = cmap.all_items() + flat = context_memory.all_items() order = {it.id: i for i, it in enumerate(flat)} victims = sorted( flat, @@ -129,10 +129,10 @@ def evict(cmap: ContextMap, scores: dict[str, int], budget: int) -> ContextMap: removed: set[str] = set() for v in victims: removed.add(v.id) - trial = cmap.without(removed) + trial = context_memory.without(removed) if count_tokens(trial.render()) <= budget: return trial - return cmap.without(removed) + return context_memory.without(removed) # --------------------------------------------------------------------------- diff --git a/src/codespy/agents/memory/hippocampus/context_map.py b/src/codespy/agents/memory/hippocampus/context_map.py deleted file mode 100644 index 7c3740a..0000000 --- a/src/codespy/agents/memory/hippocampus/context_map.py +++ /dev/null @@ -1,222 +0,0 @@ -from __future__ import annotations - -import uuid -from enum import Enum -from typing import Literal - -from pydantic import BaseModel, Field - - -class ItemTag(str, Enum): - """How a context-map item performed in the trajectory just observed. - - - helpful: directly aided orientation or answering; keep. - - harmful: misled the agent or contradicted observations; remove. - - neutral: present but unused this round; keep with no boost. - - stale: no longer reflects the external context; remove. - """ - - HELPFUL = "helpful" - HARMFUL = "harmful" - NEUTRAL = "neutral" - STALE = "stale" - - -class OpType(str, Enum): - """Cartographer edit operations against the context map.""" - - ADD = "ADD" - DELETE = "DELETE" - REPLACE = "REPLACE" - - -SectionName = Literal[ - "context_roadmap", - "context_understanding", - "domain_constants", - "parsing_schema", - "reusable_results", -] - -# Abbreviated prefixes -_SECTION_PREFIX: dict[str, str] = { - "context_roadmap": "cr", - "context_understanding": "cu", - "domain_constants": "dc", - "parsing_schema": "ps", - "reusable_results": "rr", -} - - -class Item(BaseModel): - id: str - content: str - - -class CacheCandidate(BaseModel): - section: SectionName - value: str = Field( - description="Compact candidate cache item, within the max_context_item_tokens budget." - ) - transferability: str = Field(description="Kinds of future questions this would help.") - rationale: str = Field(description="Why this is shared understanding, not a one-off fact.") - - -class Operation(BaseModel): - type: OpType - section: SectionName | None = Field(default=None, description="Required for ADD.") - item_id: str | None = Field(default=None, description="Required for DELETE / REPLACE.") - content: str | None = Field(default=None, description="Required for ADD / REPLACE.") - - -class Mutation(BaseModel): - """A recorded Cartographer mutation applied to the context map. - - Tracks the sequence of ADD/DELETE/REPLACE operations with pre-mutation - state for debugging and audit purposes. - """ - - step: int = Field(description="Which _distill() pass produced this mutation (0-indexed)") - type: OpType = Field(description="Type of mutation: ADD, DELETE, or REPLACE") - item_id: str = Field(description="Generated ID (ADD) or existing ID (DELETE/REPLACE)") - section: SectionName = Field(description="Section the item belongs to") - content: str | None = Field(default=None, description="New content (ADD/REPLACE); None for DELETE") - previous_content: str | None = Field(default=None, description="Old content (DELETE/REPLACE); None for ADD") - - -class ContextMap(BaseModel): - context_roadmap: list[Item] = Field( - default_factory=list, - description="Index of what the context contains and where to find it", - ) - context_understanding: list[Item] = Field( - default_factory=list, - description="High-level understanding of the context", - ) - domain_constants: list[Item] = Field( - default_factory=list, - description=( - "Exact parameters, formulas, thresholds, reference values, " - "enum sets, and output field requirements" - ), - ) - parsing_schema: list[Item] = Field( - default_factory=list, - description=( - "How to parse and navigate the context's format: " - "delimiters, boundary patterns, field structure" - ), - ) - reusable_results: list[Item] = Field( - default_factory=list, - description=( - "Agent-derived aggregated outputs (counts, distributions, classifications) " - "that multiple questions would need" - ), - ) - - @classmethod - def section_names(cls) -> list[str]: - return list(cls.model_fields) - - def section(self, name: str) -> list[Item]: - return getattr(self, name) - - def find_item(self, item_id: str) -> tuple[SectionName, Item] | None: - """Look up an item by ID across all sections. - - Returns: - Tuple of (section_name, item) if found, None otherwise. - """ - for sec in self.section_names(): - for it in self.section(sec): - if it.id == item_id: - return sec, it - return None - - def all_items(self) -> list[Item]: - return [it for s in self.section_names() for it in self.section(s)] - - def ids(self) -> set[str]: - return {it.id for it in self.all_items()} - - def render(self) -> str: - lines: list[str] = [] - for sec in self.section_names(): - info = type(self).model_fields[sec] - items = self.section(sec) - lines.append(f"## {sec.upper().replace('_', ' ')}") - if items: - lines.extend(f"[{it.id}] {it.content}" for it in items) - else: - lines.append(f"({info.description})") - lines.append("") - return "\n".join(lines).rstrip() + "\n" - - def apply(self, ops: list[Operation]) -> tuple[ContextMap, list[str]]: - """Return (new map, ids of newly-added items).""" - cm = self.model_copy(deep=True) - new_ids: list[str] = [] - for op in ops: - if op.type == OpType.DELETE and op.item_id: - for sec in cm.section_names(): - lst = cm.section(sec) - lst[:] = [it for it in lst if it.id != op.item_id] - elif op.type == OpType.REPLACE and op.item_id and op.content: - for sec in cm.section_names(): - lst = cm.section(sec) - for i, it in enumerate(lst): - if it.id == op.item_id: - lst[i] = Item(id=it.id, content=op.content) - elif op.type == OpType.ADD and op.section and op.content: - prefix = _SECTION_PREFIX.get(op.section, op.section[:2]) - new_id = f"{prefix}-{uuid.uuid4().hex}" - cm.section(op.section).append(Item(id=new_id, content=op.content)) - new_ids.append(new_id) - return cm, new_ids - - def without(self, ids: set[str]) -> ContextMap: - cm = self.model_copy(deep=True) - for sec in cm.section_names(): - lst = cm.section(sec) - lst[:] = [it for it in lst if it.id not in ids] - return cm - - def to_json(self) -> str: - """Serialize the map to a JSON string.""" - return self.model_dump_json(indent=2) - - @classmethod - def from_json(cls, text: str) -> ContextMap: - """Deserialize a map from a JSON string produced by ``to_json()`` .""" - return cls.model_validate_json(text) - - @classmethod - def merge(cls, *maps: "ContextMap") -> "ContextMap": - """Merge multiple context maps into a single map. - - Later maps win on ID collision (items with duplicate IDs are - replaced by those from later maps in the argument list). - - Args: - *maps: One or more ContextMap instances to merge. - - Returns: - A new ContextMap containing merged items from all input maps. - """ - merged = cls() - for cmap in maps: - for sec in cls.section_names(): - merged_section = merged.section(sec) - existing_ids = {item.id for item in merged_section} - for item in cmap.section(sec): - if item.id in existing_ids: - # Replace existing item (later wins) - merged_section[:] = [ - it if it.id != item.id else item.model_copy(deep=True) - for it in merged_section - ] - else: - merged_section.append(item.model_copy(deep=True)) - existing_ids.add(item.id) - return merged diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py new file mode 100644 index 0000000..72a9bfd --- /dev/null +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -0,0 +1,418 @@ +from __future__ import annotations + +import os +import uuid +from enum import Enum +from typing import Literal + +from pydantic import BaseModel, Field + + +class ItemTag(str, Enum): + """How a context-memory item performed in the trajectory just observed. + + - helpful: directly aided orientation or answering; keep. + - harmful: misled the agent or contradicted observations; remove. + - neutral: present but unused this round; keep with no boost. + - stale: no longer reflects the external context; remove. + """ + + HELPFUL = "helpful" + HARMFUL = "harmful" + NEUTRAL = "neutral" + STALE = "stale" + + +class OpType(str, Enum): + """Cartographer edit operations against the context memory.""" + + ADD = "ADD" + DELETE = "DELETE" + REPLACE = "REPLACE" + + +SectionName = Literal[ + "context_roadmap", + "context_understanding", + "domain_constants", + "parsing_schema", + "reusable_results", +] + +# Abbreviated prefixes +_SECTION_PREFIX: dict[str, str] = { + "context_roadmap": "cr", + "context_understanding": "cu", + "domain_constants": "dc", + "parsing_schema": "ps", + "reusable_results": "rr", +} + + +class Topic(BaseModel): + """A topic representing a scope in the repository. + + Topics are used to group context items by their relevant scope. + """ + + id: str = Field(description="Topic identifier (e.g., 'owner/repo/package-name')") + description: str = Field(description="Description of this topic's role") + + +class Item(BaseModel): + """A single item in the context memory.""" + + id: str = Field(description="Unique item identifier") + content: str = Field(description="Item content") + topic_ids: list[str] = Field(default_factory=list, description="IDs of topics this item belongs to") + + def bind_topics(self, topic_ids: list[str]) -> None: + """Bind this item to the given topic_ids (only if currently unbound).""" + if not self.topic_ids: + self.topic_ids = list(topic_ids) + + +class CacheCandidate(BaseModel): + """A candidate item to be added to the context memory.""" + + section: SectionName = Field(description="Section to add the item to") + value: str = Field(description="Compact candidate cache item, within the max_context_item_tokens budget.") + transferability: str = Field(description="Kinds of future questions this would help.") + rationale: str = Field(description="Why this is shared understanding, not a one-off fact.") + + +class Operation(BaseModel): + """A single edit operation against the context memory.""" + + type: OpType = Field(description="Type of operation") + section: SectionName | None = Field(default=None, description="Required for ADD.") + item_id: str | None = Field(default=None, description="Required for DELETE / REPLACE.") + content: str | None = Field(default=None, description="Required for ADD / REPLACE.") + + +class Mutation(BaseModel): + """A recorded Cartographer mutation applied to the context memory. + + Tracks the sequence of ADD/DELETE/REPLACE operations with pre-mutation + state for debugging and audit purposes. + """ + + step: int = Field(description="Which _distill() pass produced this mutation (0-indexed)") + type: OpType = Field(description="Type of mutation: ADD, DELETE, or REPLACE") + item_id: str = Field(description="Generated ID (ADD) or existing ID (DELETE/REPLACE)") + section: SectionName = Field(description="Section the item belongs to") + content: str | None = Field(default=None, description="New content (ADD/REPLACE); None for DELETE") + previous_content: str | None = Field(default=None, description="Old content (DELETE/REPLACE); None for ADD") + topic_ids: list[str] = Field(default_factory=list, description="Topic IDs associated with this mutation") + + +class ContextMemory(BaseModel): + """Context memory with topics and sectioned items. + + Topic-aware structure where each Item links to one or more topics via topic_ids. + """ + + topics: list[Topic] = Field(default_factory=list, description="Topics representing repo scopes") + context_roadmap: list[Item] = Field( + default_factory=list, + description="Index of what the context contains and where to find it", + ) + context_understanding: list[Item] = Field( + default_factory=list, + description="High-level understanding of the context", + ) + domain_constants: list[Item] = Field( + default_factory=list, + description=( + "Exact parameters, formulas, thresholds, reference values, " + "enum sets, and output field requirements" + ), + ) + parsing_schema: list[Item] = Field( + default_factory=list, + description=( + "How to parse and navigate the context's format: " + "delimiters, boundary patterns, field structure" + ), + ) + reusable_results: list[Item] = Field( + default_factory=list, + description=( + "Agent-derived aggregated outputs (counts, distributions, classifications) " + "that multiple questions would need" + ), + ) + + @classmethod + def section_names(cls) -> list[str]: + """Return list of section field names (excluding 'topics').""" + return [f for f in cls.model_fields if f != "topics"] + + def section(self, name: str) -> list[Item]: + """Get items from a section by name.""" + return getattr(self, name) + + def bind_topics(self, topics: list[Topic], default_topic_ids: list[str]) -> None: + """Set topics and bind all untagged items to default_topic_ids. + + Used by the scope resolver after topics are computed post-hoc + (chicken-and-egg: Hippocampus runs before topics are known). + + Args: + topics: Full list of Topic objects to set on this memory. + default_topic_ids: topic_ids to assign to any item with empty topic_ids. + """ + self.topics = topics + for sec in self.section_names(): + for item in self.section(sec): + item.bind_topics(default_topic_ids) + + def all_items(self) -> list[Item]: + """Return all items across all sections.""" + return [it for s in self.section_names() for it in self.section(s)] + + def find_item(self, item_id: str) -> tuple[SectionName, Item] | None: + """Look up an item by ID across all sections. + + Returns: + Tuple of (section_name, item) if found, None otherwise. + """ + for sec in self.section_names(): + for it in self.section(sec): + if it.id == item_id: + return sec, it # type: ignore[return-value] + return None + + def ids(self) -> set[str]: + """Return set of all item IDs.""" + return {it.id for it in self.all_items()} + + def render(self) -> str: + """Render the context memory as topic-grouped text for LLM consumption. + + Returns: + Topic-grouped text with items organized under their respective topics. + Returns empty string if ContextMemory is completely empty (no topics with items). + """ + # Build topic ID -> topic map + topic_map: dict[str, Topic] = {t.id: t for t in self.topics} + + # Categorize items + shared_items: list[tuple[SectionName, Item]] = [] + topic_items: dict[str, list[tuple[SectionName, Item]]] = {} + + for sec_name in self.section_names(): + for item in self.section(sec_name): + if not item.topic_ids or len(item.topic_ids) != 1: + # Empty or multiple topics -> SHARED + shared_items.append((sec_name, item)) + else: + topic_id = item.topic_ids[0] + if topic_id in topic_map: + if topic_id not in topic_items: + topic_items[topic_id] = [] + topic_items[topic_id].append((sec_name, item)) + else: + # Unknown topic_id -> SHARED + shared_items.append((sec_name, item)) + + # Check if completely empty + if not shared_items and not topic_items: + return "" + + lines: list[str] = [] + + # Render SHARED group first + if shared_items: + lines.append("## SHARED") + lines.extend(self._render_items_by_section(shared_items)) + lines.append("") + + # Render topic groups in order they appear in topics list + for topic in self.topics: + if topic.id in topic_items: + items = topic_items[topic.id] + lines.append(f"## TOPIC: {topic.id} ({topic.description})") + lines.extend(self._render_items_by_section(items)) + lines.append("") + + return "\n".join(lines).rstrip() + "\n" + + def _render_items_by_section( + self, items: list[tuple[SectionName, Item]] + ) -> list[str]: + """Render items grouped by section. + + Args: + items: List of (section_name, item) tuples + + Returns: + List of formatted lines + """ + # Group by section + by_section: dict[str, list[Item]] = {} + for sec_name, item in items: + if sec_name not in by_section: + by_section[sec_name] = [] + by_section[sec_name].append(item) + + lines: list[str] = [] + # Render in section order (as defined in section_names) + for sec_name in self.section_names(): + if sec_name in by_section: + sec_items = by_section[sec_name] + if sec_items: + sec_display = sec_name.upper().replace("_", " ") + lines.append(f"### {sec_display}") + for item in sec_items: + lines.append(f"[{item.id}] {item.content}") + return lines + + def apply(self, ops: list[Operation], topic_ids: list[str] | None = None) -> tuple[ContextMemory, list[str]]: + """Apply operations to create a new ContextMemory. + + Args: + ops: List of operations to apply (ADD, DELETE, REPLACE) + topic_ids: Optional list of topic IDs to assign to new items + + Returns: + Tuple of (new ContextMemory, list of IDs of newly-added items) + """ + cm = self.model_copy(deep=True) + new_ids: list[str] = [] + + for op in ops: + if op.type == OpType.DELETE and op.item_id: + for sec in cm.section_names(): + lst = cm.section(sec) + lst[:] = [it for it in lst if it.id != op.item_id] + + elif op.type == OpType.REPLACE and op.item_id and op.content: + for sec in cm.section_names(): + lst = cm.section(sec) + for i, it in enumerate(lst): + if it.id == op.item_id: + # Preserve existing topic_ids on REPLACE + lst[i] = Item(id=it.id, content=op.content, topic_ids=it.topic_ids) + + elif op.type == OpType.ADD and op.section and op.content: + prefix = _SECTION_PREFIX.get(op.section, op.section[:2]) + new_id = f"{prefix}-{uuid.uuid4().hex}" + new_item = Item(id=new_id, content=op.content, topic_ids=topic_ids or []) + cm.section(op.section).append(new_item) + new_ids.append(new_id) + + return cm, new_ids + + def without(self, ids: set[str]) -> ContextMemory: + """Return a new ContextMemory without the specified items.""" + cm = self.model_copy(deep=True) + for sec in cm.section_names(): + lst = cm.section(sec) + lst[:] = [it for it in lst if it.id not in ids] + return cm + + def to_json(self) -> str: + """Serialize the memory to a JSON string.""" + return self.model_dump_json(indent=2) + + @classmethod + def from_json(cls, text: str) -> ContextMemory: + """Deserialize a memory from a JSON string.""" + return cls.model_validate_json(text) + + @classmethod + def merge(cls, *memories: "ContextMemory") -> "ContextMemory": + """Merge multiple context memories into a single memory. + + Later memories win on ID collision (items with duplicate IDs are + replaced by those from later memories in the argument list). + + Topics are deduplicated by ID, with later non-empty descriptions winning. + + Args: + *memories: One or more ContextMemory instances to merge. + + Returns: + A new ContextMemory containing merged topics and items. + """ + merged = cls() + + # Merge topics (deduplicate by id, later non-empty description wins) + topic_map: dict[str, Topic] = {} + for mem in memories: + for topic in mem.topics: + if topic.id not in topic_map: + topic_map[topic.id] = topic + elif topic.description and not topic_map[topic.id].description: + # Later non-empty description wins + topic_map[topic.id] = topic + merged.topics = list(topic_map.values()) + + # Merge items + for mem in memories: + for sec in cls.section_names(): + merged_section = merged.section(sec) + existing_ids = {item.id for item in merged_section} + for item in mem.section(sec): + if item.id in existing_ids: + # Replace existing item (later wins) + merged_section[:] = [ + it if it.id != item.id else item.model_copy(deep=True) + for it in merged_section + ] + else: + merged_section.append(item.model_copy(deep=True)) + existing_ids.add(item.id) + + return merged + + +def make_topic_id(repo_full_name: str, subroot: str, package_name: str | None = None) -> str: + """Build topic ID from repo identity and scope info. + + Priority: + 1. package_name provided and contains owner/repo -> use from owner/repo onwards + 2. package_name provided -> {repo_full_name}/{package_name} + 3. Fallback -> {repo_full_name}/{subroot} (or just repo_full_name for root) + + Args: + repo_full_name: Repository full name (owner/repo) + subroot: Path relative to repo root + package_name: Optional package name from manifest + + Returns: + Topic ID string + """ + if package_name: + if repo_full_name in package_name: + idx = package_name.index(repo_full_name) + return package_name[idx:] + return f"{repo_full_name}/{package_name}" + if subroot in (".", ""): + return repo_full_name + return f"{repo_full_name}/{subroot.strip('/')}" + + +def compute_common_ancestor_topic_id(repo_full_name: str, subroots: list[str]) -> str | None: + """Return topic ID for deepest common ancestor when >1 scope exists. + + Args: + repo_full_name: Repository full name (owner/repo) + subroots: List of subroot paths + + Returns: + Topic ID for common ancestor, or None if <=1 scope + """ + if len(subroots) <= 1: + return None + paths = [s.rstrip("/") for s in subroots if s not in (".", "")] + if len(paths) < 2: + return make_topic_id(repo_full_name, ".") + common = os.path.commonpath(paths) + if not common or common == ".": + return make_topic_id(repo_full_name, ".") + return make_topic_id(repo_full_name, common) + + + diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 5744def..7fa52c6 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field -from codespy.agents.memory.hippocampus.context_map import ContextMap, Mutation +from codespy.agents.memory.hippocampus.context_memory import ContextMemory, Mutation from codespy.tools.storage.base import Storage @@ -14,8 +14,8 @@ class Episode(BaseModel): """A snapshot of an agent's consolidated memory at the end of an episode. Recorded by ``Hippocampus.end_episode()`` after the buffered trajectories - have been distilled into the context map. It captures *what the agent knew* - (the consolidated ``ContextMap``) together with lightweight identity and + have been distilled into the context memory. It captures *what the agent knew* + (the consolidated ``ContextMemory``) together with lightweight identity and timing metadata, so a review/run leaves behind a durable, inspectable record of the memory it produced. @@ -26,19 +26,20 @@ class Episode(BaseModel): module: Class name of the wrapped ``dspy.Module`` (e.g. ``"CodeReviewer"``). question: Question/task description derived from the first buffered call's inputs (via ``question_field`` or serialized input fields). - context_map: Deep-copied snapshot of the context map *after* - consolidation, so later edits to the live map do not mutate this + context_memory: Deep-copied snapshot of the context memory *after* + consolidation, so later edits to the live memory do not mutate this record. timestamp: UTC time the episode was recorded. artifacts: Named output artifacts produced by the wrapped agent for - this episode (e.g. ``{"review": ""}``). Agent-agnostic: - any module can attach whatever markdown/text output it produced - under a key of its choosing. Empty by default. - run_id: Identifier of the pipeline run that produced this episode. - Shared by every agent/module invoked within the same - ``ReviewPipeline.forward()`` call, so all episodes from one - review run can be correlated. Also used as the ```` prefix - in the episode filename: ``--.json``. + this episode (e.g. ``{"review": ""}``). Agent-agnostic — + any caller can attach whatever markdown/text output it + produced under a key of its choosing. Empty by default. + run_id: Identifier of the pipeline run that produced this episode. + Shared by every agent/module invoked within the same + ``ReviewPipeline.forward()`` call, so all episodes from one + review run can be correlated. Also used as the ```` prefix + in the episode filename: ``--.json``. + mutations: Ordered sequence of Cartographer mutations applied during this episode. """ run_id: str = Field( default="", @@ -58,7 +59,7 @@ class Episode(BaseModel): default_factory=dict, description="Named output artifacts produced by the agent (e.g. {'review': ''})", ) - context_map: ContextMap = Field(description="Consolidated context map snapshot") + context_memory: ContextMemory = Field(description="Consolidated context memory snapshot") mutations: list[Mutation] = Field( default_factory=list, description="Ordered sequence of Cartographer mutations applied during this episode", diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 0f4569c..b912506 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -19,7 +19,7 @@ format_inputs, format_trajectory, ) -from codespy.agents.memory.hippocampus.context_map import ContextMap, ItemTag, Mutation, Operation, OpType +from codespy.agents.memory.hippocampus.context_memory import ContextMemory, ItemTag, Mutation, Operation, OpType from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.hippocampus.episode import load_episode as _load_episode from codespy.agents.memory.hippocampus.episode import save_episode as _save_episode @@ -28,23 +28,28 @@ from codespy.tools.storage.base import Storage -def prepend_context_map(sig): +def prepend_context_memory(sig): + """Prepend context_memory field to signature. + + The context_memory is passed as a pre-rendered string to avoid + Pydantic serialization in the LLM prompt. + """ return sig.prepend( - name="context_map", + name="context_memory", field=dspy.InputField( - desc="Orientation cache about the external context. Use it before redundant tool calls." + desc="Current context memory (topic-grouped, with item IDs and sections). Use it before redundant tool calls." ), - type_=ContextMap, + type_=str, ) class Hippocampus(dspy.Module): - """Wraps a dspy.Module with a context map that evolves via LLM-driven reflection. + """Wraps a dspy.Module with a context memory that evolves via LLM-driven reflection. - The context map is prepended to every agent call so the agent starts each run + The context memory is prepended to every agent call so the agent starts each run with accumulated orientation knowledge (structure, entities, constants) about the external context. After calls, the Distiller extracts transferable - understanding and the Cartographer edits the map — "caching understanding, + understanding and the Cartographer edits the memory — "caching understanding, not answers." ## Two independent controls @@ -57,7 +62,7 @@ class Hippocampus(dspy.Module): thereafter. 2. **Calling ``end_episode()``** (or not) — whether to consolidate the buffered - episode into the map at the end. Every call is *always* buffered so + episode into the memory at the end. Every call is *always* buffered so ``end_episode()`` is available regardless of the online setting. Common patterns:: @@ -78,7 +83,7 @@ class Hippocampus(dspy.Module): pred = mem(task=task) mem.end_episode() - # Read-only (map never changes) — pure inference + # Read-only (memory never changes) — pure inference mem = Hippocampus(agent, max_reflects=0) pred = mem(task="…") # no end_episode() call @@ -114,7 +119,8 @@ def __init__( question: str | None = None, task_name: str | None = None, run_id: str | None = None, - initial_memory: ContextMap | None = None, + initial_memory: ContextMemory | None = None, + topic_ids: list[str] | None = None, ): """ Args: @@ -148,27 +154,29 @@ class for per-field guidance. Resolve one from configuration with used as the ```` prefix in the episode filename (``-.json``) and recorded on ``Episode.run_id``. If ``None`` (standalone usage), a random UUID is generated. - initial_memory: Optional context map to seed the agent with. When - provided, the agent starts with this map instead of an empty one, + initial_memory: Optional context memory to seed the agent with. When + provided, the agent starts with this memory instead of an empty one, inheriting accumulated understanding from upstream pipeline stages. + topic_ids: Optional list of topic IDs to auto-assign to all new items + created during this episode. Used for scope-aware memory organization. """ super().__init__() module = copy.deepcopy(module) - # Prepend context_map only to predictors that receive the module's own + # Prepend context_memory only to predictors that receive the module's own # input fields. top_sig = getattr(module, "signature", None) if top_sig is not None: module_inputs = set(top_sig.input_fields) - module.signature = prepend_context_map(top_sig) + module.signature = prepend_context_memory(top_sig) for _, pred in module.named_predictors(): if set(pred.signature.input_fields) & module_inputs: - if "context_map" not in pred.signature.input_fields: - pred.signature = prepend_context_map(pred.signature) + if "context_memory" not in pred.signature.input_fields: + pred.signature = prepend_context_memory(pred.signature) else: for _, pred in module.named_predictors(): - pred.signature = prepend_context_map(pred.signature) + pred.signature = prepend_context_memory(pred.signature) self.agent = module self.distill = Distiller() @@ -176,7 +184,8 @@ class for per-field guidance. Resolve one from configuration with self.budget = budget or MemoryBudget() self.max_reflects = max_reflects self.question = question - self.cmap = initial_memory.model_copy(deep=True) if initial_memory else ContextMap() + self.cmem = initial_memory.model_copy(deep=True) if initial_memory else ContextMemory() + self._topic_ids = topic_ids or [] self.scores: dict[str, int] = {} # Buffer of per-call bounded trajectory strings, cleared after end_episode(). self._episode_trajectories: list[str] = [] @@ -219,11 +228,12 @@ class for per-field guidance. Resolve one from configuration with self._distill_step: int = 0 @property - def current_map_text(self) -> str: - return self.cmap.render() + def current_memory_text(self) -> str: + """Return the rendered context memory as text.""" + return self.cmem.render() def forward(self, **kwargs) -> dspy.Prediction: - pred = self.agent(context_map=self.cmap, **kwargs) + pred = self.agent(context_memory=self.cmem.render(), **kwargs) self._buffer_and_distill(pred, kwargs) return pred @@ -236,7 +246,7 @@ async def aforward(self, **kwargs) -> dspy.Prediction: The Distiller/Cartographer reflection pass is still synchronous under the hood but is offloaded to a thread so it never blocks the loop. """ - pred = await self.agent.acall(context_map=self.cmap, **kwargs) + pred = await self.agent.acall(context_memory=self.cmem.render(), **kwargs) await asyncio.to_thread(self._buffer_and_distill, pred, kwargs) return pred @@ -299,7 +309,7 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: task=self._task_name, module=self._module_name, question=self._episode_question or "", - context_map=self.cmap.model_copy(deep=True), + context_memory=self.cmem.model_copy(deep=True), timestamp=datetime.now(UTC), artifacts=artifacts or {}, run_id=self._run_id, @@ -336,7 +346,7 @@ def end_episode( dir: str | None = None, artifacts: dict[str, str] | None = None, ) -> None: - """Consolidate the buffered trajectories into the map and record an Episode snapshot. + """Consolidate the buffered trajectories into the memory and record an Episode snapshot. A single Distiller pass sees all buffered trajectories joined with ``=== Call k ===`` headers. If ``budget.max_trajectory_tokens`` is set, the @@ -346,7 +356,7 @@ def end_episode( After consolidation ``self.episode`` is set to a new :class:`Episode` containing the task/module identity and a deep-copy snapshot of the - updated context map. + updated context memory. If both ``store`` and ``dir`` are provided the episode is persisted via ``save_episode()`` after consolidation, at @@ -427,8 +437,8 @@ def save_episode(self, store: Storage, path: str) -> None: def load_episode(self, store: Storage, path: str) -> None: """Replace the current state with an episode loaded from ``path`` via ``store``. - Restores both ``self.episode`` and the live context map - (``self.cmap = episode.context_map``) so the agent resumes from the + Restores both ``self.episode`` and the live context memory + (``self.cmem = episode.context_memory``) so the agent resumes from the persisted state. Also resets ``scores`` and clears the trajectory buffer since they belong to the previous state. @@ -442,7 +452,7 @@ def load_episode(self, store: Storage, path: str) -> None: """ ep = _load_episode(store, path) self.episode = ep - self.cmap = ep.context_map + self.cmem = ep.context_memory self.scores = {} self._episode_trajectories.clear() self._episode_question = None @@ -470,7 +480,7 @@ def _make_question(self, inputs: dict) -> str: return format_inputs(inputs, self.budget.max_question_tokens) def _record_mutations( - self, ops: list[Operation], new_ids: list[str], pre_map: ContextMap + self, ops: list[Operation], new_ids: list[str], pre_memory: ContextMemory ) -> list[Mutation]: """Build Mutation records from operations and the new IDs generated by apply(). @@ -480,7 +490,7 @@ def _record_mutations( Args: ops: Cartographer operations (ADD/DELETE/REPLACE). new_ids: IDs of items created by apply() in the same order as ADD ops. - pre_map: Context map state before apply() — used to look up + pre_memory: Context memory state before apply() — used to look up previous content for DELETE/REPLACE. Returns: @@ -490,7 +500,7 @@ def _record_mutations( add_indices: list[int] = [] for op in ops: if op.type == OpType.DELETE and op.item_id: - found = pre_map.find_item(op.item_id) + found = pre_memory.find_item(op.item_id) if found: section, old_item = found mutations.append( @@ -501,10 +511,11 @@ def _record_mutations( section=section, content=None, previous_content=old_item.content, + topic_ids=old_item.topic_ids, ) ) elif op.type == OpType.REPLACE and op.item_id and op.content: - found = pre_map.find_item(op.item_id) + found = pre_memory.find_item(op.item_id) if found: section, old_item = found mutations.append( @@ -515,6 +526,7 @@ def _record_mutations( section=section, content=op.content, previous_content=old_item.content, + topic_ids=old_item.topic_ids, ) ) elif op.type == OpType.ADD and op.section and op.content: @@ -527,6 +539,7 @@ def _record_mutations( section=op.section, content=op.content, previous_content=None, + topic_ids=list(self._topic_ids), ) ) # Back-fill ADD mutation item_ids from new_ids @@ -537,12 +550,12 @@ def _record_mutations( def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, - context_map=self.cmap, + context_memory=self.cmem.render(), question=question, max_context_item_tokens=self.budget.max_context_item_tokens, ) - known = self.cmap.ids() + known = self.cmem.ids() tags = {k: v for k, v in (distilled.item_tags or {}).items() if k in known} for bid, tag in tags.items(): if tag == ItemTag.HELPFUL: @@ -556,26 +569,26 @@ def _distill(self, trajectory: str, question: str) -> None: diagnosis=distilled.diagnosis, item_tags=tags, cache_candidates=list(distilled.cache_candidates or []), - current_map=self.cmap, + current_map=self.cmem.render(), question=question, # The Cartographer's input field keeps the generic name: it is prompt # text, already scoped by its description, and pairs with current_tokens. - token_budget=self.budget.max_context_map_tokens, - current_tokens=count_tokens(self.cmap.render()), + token_budget=self.budget.max_context_memory_tokens, + current_tokens=count_tokens(self.cmem.render()), max_context_item_tokens=self.budget.max_context_item_tokens, ) ops = list(edits.operations or []) if ops: - pre_map = self.cmap - self.cmap, new_ids = self.cmap.apply(ops) - mutations = self._record_mutations(ops, new_ids, pre_map) + pre_memory = self.cmem + self.cmem, new_ids = self.cmem.apply(ops, topic_ids=self._topic_ids) + mutations = self._record_mutations(ops, new_ids, pre_memory) self._mutations.extend(mutations) for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 self._distill_step += 1 - self.cmap = evict(self.cmap, self.scores, self.budget.max_context_map_tokens) + self.cmem = evict(self.cmem, self.scores, self.budget.max_context_memory_tokens) - live = self.cmap.ids() + live = self.cmem.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} diff --git a/src/codespy/agents/memory/hippocampus/modules/__init__.py b/src/codespy/agents/memory/hippocampus/modules/__init__.py index 4bb65b6..1099e60 100644 --- a/src/codespy/agents/memory/hippocampus/modules/__init__.py +++ b/src/codespy/agents/memory/hippocampus/modules/__init__.py @@ -1,6 +1,6 @@ """DSPy modules for the hippocampus agent.""" -from codespy.agents.memory.hippocampus.context_map import Mutation +from codespy.agents.memory.hippocampus.context_memory import Mutation from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index ec85a08..d6828a9 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -2,20 +2,19 @@ import dspy -from codespy.agents.memory.hippocampus.context_map import ( +from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, - ContextMap, ItemTag, Operation, ) class CartographerSig(dspy.Signature): - """You are a context map curator. You maintain a concise, high-value - context map prepended to an agent that repeatedly interacts with a long + """You are a context memory curator. You maintain a concise, high-value + context memory prepended to an agent that repeatedly interacts with a long external context. - The context map captures the agent's evolving UNDERSTANDING of the + The context memory captures the agent's evolving UNDERSTANDING of the context — NOT answers to specific questions. Think of it as the mental model a human builds after reading a document: structure, key entities, relationships, and global summaries that help with ANY question about @@ -23,7 +22,7 @@ class CartographerSig(dspy.Signature): ## Instructions - - Review the latest Distiller diagnosis and the current context map. + - Review the latest Distiller diagnosis and the current context memory. - Prioritize items representing SHARED UNDERSTANDING — knowledge useful across many different questions on this context. - Demote or remove question-specific facts that only help one query. @@ -56,7 +55,7 @@ class CartographerSig(dspy.Signature): - ADD: requires `section` (one of the five section names) and `content`. - DELETE: requires `item_id`. - REPLACE: requires `item_id` and `content`. - - Only reference `item_id`s that exist in the current map. Never invent + - Only reference `item_id`s that exist in the current memory. Never invent ids — new items get their ids assigned automatically on ADD. ## Value Priority (highest to lowest) @@ -106,18 +105,18 @@ class CartographerSig(dspy.Signature): cache_candidates: list[CacheCandidate] = dspy.InputField( desc="Candidate items the Distiller proposed." ) - current_map: ContextMap = dspy.InputField(desc="Current context map.") + current_map: str = dspy.InputField(desc="Current context memory (topic-grouped, with item IDs and sections).") question: str = dspy.InputField(desc="Question the agent was answering.") - token_budget: int = dspy.InputField(desc="Hard token budget for the context map.") - current_tokens: int = dspy.InputField(desc="Current token count of the context map.") + token_budget: int = dspy.InputField(desc="Hard token budget for the context memory.") + current_tokens: int = dspy.InputField(desc="Current token count of the context memory.") max_context_item_tokens: int = dspy.InputField( - desc="Token budget for a SINGLE context-map item. Every ADD/REPLACE content " + desc="Token budget for a SINGLE context memory item. Every ADD/REPLACE content " "must stay within it." ) justification: str = dspy.OutputField( desc="Brief explanation of why these edits improve the shared understanding " - "cached in the context map." + "cached in the context memory." ) operations: list[Operation] = dspy.OutputField( @@ -128,7 +127,7 @@ class CartographerSig(dspy.Signature): class Cartographer(dspy.Module): """Translates the Distiller's structured reflection into concrete edits - against the context map. + against the context memory. Owns *what is worth keeping* — selects which tagged items to drop, which candidates to add, and which existing items to rewrite. Token-budget @@ -160,4 +159,3 @@ def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, current_tokens=current_tokens, max_context_item_tokens=max_context_item_tokens, ) - diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 7c122ea..a75f1c6 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -2,9 +2,8 @@ import dspy -from codespy.agents.memory.hippocampus.context_map import ( +from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, - ContextMap, ItemTag, ) @@ -16,7 +15,7 @@ class DistillerSig(dspy.Signature): ## Key Principle: Cache Understanding, Not Answers - The context map prepended to the agent is a compact CACHE OF + The context memory prepended to the agent is a compact CACHE OF UNDERSTANDING about the external context — NOT answers to specific questions. Think of it as the mental model a human builds after reading a document: structure, key entities, relationships, and global summaries @@ -47,7 +46,7 @@ class DistillerSig(dspy.Signature): - What kind of contextual understanding the agent built that could transfer to future questions - 2. ITEM_TAGS — For EVERY item currently in the map, tag it exactly: + 2. ITEM_TAGS — For EVERY item currently in the context memory, tag it exactly: - helpful: directly helped or would directly help this run - harmful: misleading, incorrect, or actively hurts performance - neutral: correct domain knowledge not relevant to THIS question @@ -98,7 +97,7 @@ class DistillerSig(dspy.Signature): names/types — these are domain constants and must remain precise. Assign each candidate to one of these exact section names (they map - onto the context map schema): context_understanding, domain_constants, + onto the context memory schema): context_understanding, domain_constants, context_roadmap, reusable_results, parsing_schema. Each candidate is a JSON object with exactly these fields: @@ -115,10 +114,10 @@ class DistillerSig(dspy.Signature): """ trajectory: str = dspy.InputField(desc="The agent's full execution trajectory.") - context_map: ContextMap = dspy.InputField(desc="Current context map (with item IDs).") + context_memory: str = dspy.InputField(desc="Current context memory (topic-grouped, with item IDs).") question: str = dspy.InputField(desc="The question the agent was answering.") max_context_item_tokens: int = dspy.InputField( - desc="Token budget for a SINGLE context-map item. Keep every candidate within " + desc="Token budget for a SINGLE context memory item. Keep every candidate within " "it; if one exceeds it, rewrite it more compactly or split it." ) @@ -128,7 +127,7 @@ class DistillerSig(dspy.Signature): "what transferable understanding the agent built. Feeds the Cartographer prompt." ) item_tags: dict[str, ItemTag] = dspy.OutputField( - desc="Per-item-id tag for EVERY item currently in the context map. " + desc="Per-item-id tag for EVERY item currently in the context memory. " "Keys must match existing item ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( @@ -141,7 +140,7 @@ class DistillerSig(dspy.Signature): class Distiller(dspy.Module): """Extracts transferable orientation knowledge from an agent trajectory. - The context map is a prompt-resident cache of *understanding*, not + The context memory is a prompt-resident cache of *understanding*, not answers. The Distiller separates orientation work (what the context contains, how it's organized, which constants matter) from question- specific work, tags every existing item, and proposes new candidates. @@ -157,7 +156,7 @@ def __init__(self): def forward( self, trajectory: str, - context_map: ContextMap, + context_memory: str, question: str, max_context_item_tokens: int, ): @@ -171,8 +170,7 @@ def forward( with SignatureContext(self.SIGNATURE, get_cost_tracker()): return self.predict( trajectory=trajectory, - context_map=context_map, + context_memory=context_memory, question=question, max_context_item_tokens=max_context_item_tokens, ) - diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index fd6de06..fe5bdda 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, field_validator -from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.memory.hippocampus import ContextMemory class PRContext(BaseModel): @@ -26,13 +26,13 @@ class PRContext(BaseModel): class ReviewContext(BaseModel): """Evolving pipeline state threaded through review stages. - Carries both the immutable PR identity and the inherited context map - (memory) from upstream pipeline stages. Updated at each stage boundary + Carries both the immutable PR identity and the inherited context memory + from upstream pipeline stages. Updated at each stage boundary so downstream modules inherit accumulated understanding. """ pr_context: PRContext = Field(description="Immutable PR identity (repo, number, title, summary)") - memory: ContextMap | None = Field(default=None, description="Inherited context map from upstream stages") + memory: ContextMemory | None = Field(default=None, description="Inherited context memory from upstream stages") class IssueSeverity(str, Enum): @@ -74,6 +74,7 @@ class PackageManifest(BaseModel): dependencies_changed: bool = Field( default=False, description="Whether PR modified this manifest or lock file" ) + package_name: str | None = Field(default=None, description="Package identity from manifest") from codespy.tools.git.models import ChangedFile @@ -104,6 +105,9 @@ class ScopeResult(BaseModel): skills: str | None = Field( default=None, description="Project/scope instructions inherited from ancestor directories" ) + description: str = Field( + default="", description="Description of scope's role in the project (max 500 chars)" + ) model_config = {"arbitrary_types_allowed": True} @@ -117,6 +121,21 @@ def scope_path(self) -> str: return f"/{self.repo}/" return f"/{self.repo}/{self.subroot.strip('/')}/" + def topic(self, repo_full_name: str) -> "Topic": + """Build the Topic for this scope. + + Args: + repo_full_name: Repository full name (owner/repo) + + Returns: + Topic object with id and description + """ + from codespy.agents.memory.hippocampus.context_memory import make_topic_id, Topic + + package_name = self.package_manifest.package_name if self.package_manifest else None + topic_id = make_topic_id(repo_full_name, self.subroot, package_name) + return Topic(id=topic_id, description=self.description) + class Issue(BaseModel): """Represents a single issue found during review.""" diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index d90f817..96cf040 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -6,7 +6,7 @@ import dspy from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, ReviewContext from codespy.config import get_settings from codespy.config_memory import get_memory_store @@ -61,6 +61,7 @@ def forward( all_issues: Sequence[Issue], run_id: str | None = None, scopes: list["ScopeResult"] | None = None, + topic_ids: list[str] | None = None, ) -> tuple[str, str]: """Assess quality and recommend action. @@ -70,6 +71,7 @@ def forward( all_issues: All issues found during review run_id: Pipeline run identifier scopes: List of resolved scopes for per-scope episode persistence + topic_ids: Optional list of topic IDs for auto-tagging Returns: Tuple of (quality_assessment, recommendation) @@ -100,6 +102,7 @@ def forward( task_name="audit", run_id=run_id, initial_memory=review_context.memory, + topic_ids=topic_ids, ) result = mem( mr_title=review_context.pr_context.mr_title, diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 4d46206..82c8527 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,9 +8,9 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult +from codespy.tools.git.models import MergeRequest from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -186,7 +186,8 @@ async def aforward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for defects, security issues, and code smells. Args: @@ -195,9 +196,10 @@ async def aforward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ if not self._settings.is_signature_enabled("code_review"): logger.debug("Skipping code_review: disabled") @@ -215,7 +217,8 @@ async def aforward( return [], review_context.memory if review_context else None all_issues: list[Issue] = [] - scope_memories: list[ContextMap] = [] + scope_memories: list[ContextMemory] = [] + max_iters = self._settings.get_max_iters("code_review") max_iters = self._settings.get_max_iters("code_review") total_files = sum(len(s.changed_files) for s in changed_scopes) @@ -245,6 +248,7 @@ async def aforward( f"review code change of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" ) if review_context else None + topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( agent, budget=self._settings.get_memory_budget("code_review"), @@ -253,6 +257,7 @@ async def aforward( task_name="code_review", run_id=run_id, initial_memory=review_context.memory if review_context else None, + topic_ids=topic_ids, ) result = await mem.aforward( scope=scoped, @@ -267,9 +272,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) - # Collect scope's context map + # Collect scope's context memory if mem: - scope_memories.append(mem.cmap.model_copy(deep=True)) + scope_memories.append(mem.cmem.model_copy(deep=True)) else: result = await agent.acall( scope=scoped, @@ -290,9 +295,9 @@ async def aforward( await cleanup_mcp_contexts(contexts) logger.info(f"Code review found {len(all_issues)} issues") - # Merge all scope context maps into one module-level map + # Merge all scope context memories into one module-level memory merged_memory = ( - ContextMap.merge(*scope_memories) + ContextMemory.merge(*scope_memories) if scope_memories else (review_context.memory if review_context else None) ) @@ -304,7 +309,8 @@ def forward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for code issues (sync wrapper). Args: @@ -313,8 +319,9 @@ def forward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index f7455e5..273eee1 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -8,9 +8,9 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult +from codespy.tools.git.models import MergeRequest from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -119,7 +119,8 @@ async def aforward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for documentation issues. Args: @@ -128,9 +129,10 @@ async def aforward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ if not self._settings.is_signature_enabled("doc"): logger.debug("Skipping doc: disabled") @@ -140,7 +142,7 @@ async def aforward( logger.info("No scopes with changes for doc review") return [], review_context.memory if review_context else None all_issues: list[Issue] = [] - scope_memories: list[ContextMap] = [] + scope_memories: list[ContextMemory] = [] total_files = sum(len(s.changed_files) for s in changed_scopes) logger.info( f"Doc review for {len(changed_scopes)} scopes " @@ -184,6 +186,7 @@ async def aforward( f"review documentation of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" ) if review_context else None + topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( reviewer, budget=self._settings.get_memory_budget("doc"), @@ -192,6 +195,7 @@ async def aforward( task_name="doc", run_id=run_id, initial_memory=review_context.memory if review_context else None, + topic_ids=topic_ids, ) result = await mem.aforward( patches=patches, @@ -207,9 +211,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) - # Collect scope's context map + # Collect scope's context memory if mem: - scope_memories.append(mem.cmap.model_copy(deep=True)) + scope_memories.append(mem.cmem.model_copy(deep=True)) else: result = await asyncio.to_thread( reviewer, @@ -230,9 +234,9 @@ async def aforward( logger.error(f"Doc review failed for scope {scope.subroot}: {e}", exc_info=True) logger.info(f"Doc review found {len(all_issues)} issues") - # Merge all scope context maps into one module-level map + # Merge all scope context memories into one module-level memory merged_memory = ( - ContextMap.merge(*scope_memories) + ContextMemory.merge(*scope_memories) if scope_memories else (review_context.memory if review_context else None) ) @@ -244,7 +248,8 @@ def forward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for documentation issues (sync wrapper). Args: @@ -253,8 +258,9 @@ def forward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index d1fa5d3..51e6e31 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -207,7 +207,7 @@ def issues_to_markdown(issues: list[Issue]) -> str: Intended as an ``Episode`` artifact (see ``Hippocampus.aend_episode``) so a scope's episode carries a human-readable snapshot of what the - module found for that call, alongside the consolidated context map. + module found for that call, alongside the consolidated context memory. Args: issues: Issues found for a given scope/call. diff --git a/src/codespy/agents/reviewer/modules/manifest_parser.py b/src/codespy/agents/reviewer/modules/manifest_parser.py new file mode 100644 index 0000000..12d6745 --- /dev/null +++ b/src/codespy/agents/reviewer/modules/manifest_parser.py @@ -0,0 +1,307 @@ +"""Package manifest parser for extracting package names from various manifest files.""" + +from __future__ import annotations + +import configparser +import json +import re +from pathlib import Path +from xml.etree import ElementTree as ET + + +def extract_package_name(manifest_path: str, repo_path: Path) -> str | None: + """Extract package name from manifest file. Returns None on failure. + + Args: + manifest_path: Relative path to manifest file from repo root + repo_path: Path to the repository root + + Returns: + Package name string or None if extraction fails + """ + full_path = repo_path / manifest_path + if not full_path.exists(): + return None + + filename = Path(manifest_path).name + + try: + if filename == "package.json": + return _extract_from_json(full_path, ["name"]) + elif filename == "composer.json": + return _extract_from_json(full_path, ["name"]) + elif filename == "go.mod": + return _extract_from_go_mod(full_path) + elif filename == "pyproject.toml": + return _extract_from_pyproject_toml(full_path) + elif filename == "Cargo.toml": + return _extract_from_toml(full_path, ["package", "name"]) + elif filename == "pubspec.yaml": + return _extract_from_yaml(full_path, ["name"]) + elif filename == "Chart.yaml": + return _extract_from_yaml(full_path, ["name"]) + elif filename == "pom.xml": + return _extract_from_pom_xml(full_path) + elif filename == "setup.cfg": + return _extract_from_setup_cfg(full_path) + elif filename.endswith(".csproj") or filename.endswith(".fsproj") or filename.endswith(".vbproj"): + return _extract_from_dotnet_proj(full_path) + elif filename in ("build.gradle", "build.gradle.kts"): + return _extract_from_gradle(repo_path, manifest_path) + elif filename == "Gemfile": + return _extract_from_gemfile(full_path) + elif filename == "Package.swift": + return _extract_from_swift_package(full_path) + elif filename == "mix.exs": + return _extract_from_mix_exs(full_path) + except Exception: + return None + + return None + + +def _extract_from_json(path: Path, keys: list[str]) -> str | None: + """Extract value from JSON file following key path.""" + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + value = data + for key in keys: + if not isinstance(value, dict): + return None + value = value.get(key) + if value is None: + return None + return value if isinstance(value, str) else None + except (json.JSONDecodeError, UnicodeDecodeError, OSError): + return None + + +def _extract_from_go_mod(path: Path) -> str | None: + """Extract module name from go.mod file.""" + try: + with open(path, "r", encoding="utf-8") as f: + first_line = f.readline().strip() + match = re.match(r"^module\s+(\S+)", first_line) + return match.group(1) if match else None + except (UnicodeDecodeError, OSError): + return None + + +def _extract_from_toml(path: Path, keys: list[str]) -> str | None: + """Extract value from TOML file following key path.""" + try: + import tomllib + + with open(path, "rb") as f: + data = tomllib.load(f) + value = data + for key in keys: + if not isinstance(value, dict): + return None + value = value.get(key) + if value is None: + return None + return value if isinstance(value, str) else None + except Exception: + return None + + +def _extract_from_pyproject_toml(path: Path) -> str | None: + """Extract package name from pyproject.toml. + + Tries [project][name] first, then [tool][poetry][name]. + """ + try: + import tomllib + + with open(path, "rb") as f: + data = tomllib.load(f) + + # Try [project][name] first + project = data.get("project") + if isinstance(project, dict): + name = project.get("name") + if isinstance(name, str): + return name + + # Fall back to [tool][poetry][name] + tool = data.get("tool") + if isinstance(tool, dict): + poetry = tool.get("poetry") + if isinstance(poetry, dict): + name = poetry.get("name") + if isinstance(name, str): + return name + + return None + except Exception: + return None + + +def _extract_from_yaml(path: Path, keys: list[str]) -> str | None: + """Extract value from YAML file following key path.""" + try: + import yaml + + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + value = data + for key in keys: + if not isinstance(value, dict): + return None + value = value.get(key) + if value is None: + return None + return value if isinstance(value, str) else None + except Exception: + return None + + +def _extract_from_pom_xml(path: Path) -> str | None: + """Extract package name from Maven pom.xml as 'groupId:artifactId'.""" + try: + tree = ET.parse(path) + root = tree.getroot() + + # Handle namespaced XML + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + + group_id = root.find("m:groupId", ns) + if group_id is None: + group_id = root.find("groupId") + + artifact_id = root.find("m:artifactId", ns) + if artifact_id is None: + artifact_id = root.find("artifactId") + + if group_id is not None and artifact_id is not None: + return f"{group_id.text}:{artifact_id.text}" + elif artifact_id is not None: + return artifact_id.text + return None + except ET.ParseError: + return None + + +def _extract_from_setup_cfg(path: Path) -> str | None: + """Extract package name from setup.cfg [metadata] section.""" + try: + config = configparser.ConfigParser() + config.read(path, encoding="utf-8") + if config.has_option("metadata", "name"): + return config.get("metadata", "name") + return None + except (configparser.Error, UnicodeDecodeError, OSError): + return None + + +def _extract_from_dotnet_proj(path: Path) -> str | None: + """Extract package name from .NET project file. + + Tries PackageId first, then RootNamespace, then filename stem. + """ + try: + tree = ET.parse(path) + root = tree.getroot() + + # Handle namespaced XML + ns = {"p": "http://schemas.microsoft.com/developer/msbuild/2003"} + + # Try PackageId + package_id = root.find(".//p:PackageId", ns) + if package_id is None: + package_id = root.find(".//PackageId") + if package_id is not None and package_id.text: + return package_id.text + + # Try RootNamespace + root_ns = root.find(".//p:RootNamespace", ns) + if root_ns is None: + root_ns = root.find(".//RootNamespace") + if root_ns is not None and root_ns.text: + return root_ns.text + + # Fall back to filename stem + return path.stem + except ET.ParseError: + return path.stem + + +def _extract_from_gradle(repo_path: Path, manifest_path: str) -> str | None: + """Extract project name from Gradle settings.gradle or settings.gradle.kts. + + Gradle projects don't store the name in build.gradle - it's in settings.gradle. + This is a best-effort extraction. + """ + manifest_dir = Path(manifest_path).parent + + # Look for settings.gradle or settings.gradle.kts + settings_files = ["settings.gradle", "settings.gradle.kts"] + + for settings_file in settings_files: + settings_path = repo_path / manifest_dir / settings_file + if settings_path.exists(): + try: + with open(settings_path, "r", encoding="utf-8") as f: + content = f.read() + # Look for rootProject.name = 'name' or rootProject.name = "name" + match = re.search( + r"rootProject\.name\s*=\s*['\"]([^'\"]+)['\"]", + content, + ) + if match: + return match.group(1) + except (UnicodeDecodeError, OSError): + continue + + return None + + +def _extract_from_gemfile(path: Path) -> str | None: + """Extract gem name from Gemfile. + + Looks for 'source' line to extract the gem name, but this is often + not present. Returns None as Gemfiles don't reliably contain a package name. + """ + # Gemfiles don't typically contain a reliable package name + # They reference gems to install, not the current package name + return None + + +def _extract_from_swift_package(path: Path) -> str | None: + """Extract package name from Package.swift. + + Looks for 'name: "..."' in the Package initialization. + """ + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + # Look for Package(name: "...") + match = re.search( + r"Package\s*\([^)]*name:\s*['\"]([^'\"]+)['\"]", + content, + re.DOTALL, + ) + if match: + return match.group(1) + return None + except (UnicodeDecodeError, OSError): + return None + + +def _extract_from_mix_exs(path: Path) -> str | None: + """Extract project name from mix.exs. + + Looks for 'def project do' and extracts the 'app:' value. + """ + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + # Look for app: :name or app: "name" + match = re.search(r"app:\s*[:\"]([^\"\s,)]+)[\"\s,)]", content) + if match: + return match.group(1) + return None + except (UnicodeDecodeError, OSError): + return None diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 6805bcb..49ee30a 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import ContextMap, Hippocampus +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import ( PackageManifest, ReviewContext, @@ -31,6 +31,7 @@ from codespy.tools.git.client import get_client from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server +from codespy.agents.reviewer.modules.manifest_parser import extract_package_name logger = logging.getLogger(__name__) @@ -257,6 +258,10 @@ class ScopeBoundary(BaseModel): subroot: str = Field(description="Path relative to repo root (e.g., 'packages/auth' or '.' for root)") scope_type: ScopeType = Field(description="Type of scope") reason: str = Field(description="Brief explanation for this boundary") + description: str = Field( + default="", + description="Brief description (max 500 chars) of what this scope/folder contains and its role in the project" + ) class ScopeRefinementSignature(dspy.Signature): @@ -302,6 +307,11 @@ class ScopeRefinementSignature(dspy.Signature): 5. When in doubt, MERGE into fewer scopes rather than split. OUTPUT: Final refined scope boundaries. Files are assigned automatically. + + For each scope boundary, include a `description` (max 500 characters) summarizing + what the folder contains and its role in the project. For example: + - "Auth library handling JWT issuance and session management" + - "API gateway routing to downstream services" """ candidates: str = dspy.InputField( @@ -317,7 +327,8 @@ class ScopeRefinementSignature(dspy.Signature): ) scopes: list[ScopeBoundary] = dspy.OutputField( - desc="Scope boundaries (subroots). Files are assigned automatically — do NOT list files." + desc="Scope boundaries (subroots) with descriptions. Files are assigned automatically — do NOT list files. " + "For each scope boundary, include a `description` (max 500 characters) summarizing what the folder contains and its role in the project." ) @@ -556,6 +567,12 @@ def _resolve( deps_changed = self._dependencies_changed(manifest_dir, manifest_filename, lock_file, changed_paths) manifest_path = str(manifest_dir / manifest_filename) if manifest_dir != Path(".") else manifest_filename + # Extract package name from manifest + package_name = extract_package_name(manifest_path, repo_path) + + # Build deterministic description + description = f"{pkg_mgr} package at {subroot}" if subroot != "." else f"{pkg_mgr} package (root)" + scopes[subroot] = ScopeResult( repo=repo, subroot=subroot, @@ -565,8 +582,10 @@ def _resolve( lock_file_path=str(lock_file) if lock_file else None, package_manager=pkg_mgr, dependencies_changed=deps_changed, + package_name=package_name, ), reason=f"manifest {manifest_filename} at {subroot}/", + description=description, ) has_nested_manifests = any(subroot != "." for subroot in scopes) @@ -582,11 +601,14 @@ def _resolve( indicator_type, indicator_path = self._find_scope_indicator(file.filename) if indicator_path and indicator_type and indicator_path not in scopes: + # Build deterministic description for indicator-based scope + description = f"{indicator_type.value} scope at {indicator_path}" scopes[indicator_path] = ScopeResult( repo=repo, subroot=indicator_path, scope_type=indicator_type, reason="scope indicator in path (no parent manifest)", + description=description, ) # Assign files to deepest matching scope @@ -882,7 +904,7 @@ async def _refine_scopes( repo_path: Path, review_context: ReviewContext | None, run_id: str | None, - ) -> list[ScopeResult]: + ) -> tuple[list[ScopeResult], "ContextMemory | None"]: """Use ReAct agent to refine scope assignments from deterministic candidates. Args: @@ -894,8 +916,11 @@ async def _refine_scopes( run_id: Pipeline run identifier Returns: - List of ScopeResult with agent-resolved assignments + Tuple of (list of ScopeResult with agent-resolved assignments, + ContextMemory with topics and items from Hippocampus) """ + from codespy.agents.memory.hippocampus import ContextMemory, Topic, compute_common_ancestor_topic_id + # Build candidates string from already-resolved scopes candidates_str = "\n".join(self._format_candidate(s) for s in scopes) @@ -953,10 +978,56 @@ async def _refine_scopes( result.scopes, all_files, scopes, mr.repo_slug ) + # Copy ScopeBoundary.description to ScopeResult.description (overrides deterministic fallback) + boundary_descriptions: dict[str, str] = {b.subroot: b.description for b in result.scopes} + for scope in final_scopes: + if scope.subroot in boundary_descriptions: + scope.description = boundary_descriptions[scope.subroot] + + # Build topics from final scopes + scope_topics: list[Topic] = [] + for scope in final_scopes: + topic = scope.topic(mr.repo_full_name) + scope_topics.append(topic) + + # Compute common ancestor topic if >1 scope + common_ancestor_topic_id = compute_common_ancestor_topic_id( + mr.repo_full_name, [s.subroot for s in final_scopes] + ) + if common_ancestor_topic_id: + # Build description: "Common context for scopes: subroot1, subroot2, ..." + subroot_list = ", ".join(s.subroot for s in final_scopes) + common_desc = f"Common context for scopes: {subroot_list}" + scope_topics.append(Topic(id=common_ancestor_topic_id, description=common_desc)) + stamp_topic_ids = [common_ancestor_topic_id] + elif scope_topics: + # Single scope: stamp with its topic ID + stamp_topic_ids = [scope_topics[0].id] + else: + stamp_topic_ids = [] + # Attach hierarchical skills to each produced scope for scope in final_scopes: scope.skills = collect_skills(repo_path, scope.subroot) + # Bind topics to hippocampus cmem BEFORE building context_memory. + # This ensures: (a) persisted episode includes topics, (b) items + # copied into context_memory are pre-stamped with topic_ids, + # (c) any new items from consolidation also get topic_ids via _topic_ids. + if mem is not None and stamp_topic_ids: + mem._topic_ids = stamp_topic_ids + mem.cmem.bind_topics(scope_topics, stamp_topic_ids) + + # Build ContextMemory from Hippocampus cmem (items already stamped) + context_memory = ContextMemory( + topics=scope_topics, + context_roadmap=mem.cmem.context_roadmap.copy() if mem else [], + context_understanding=mem.cmem.context_understanding.copy() if mem else [], + domain_constants=mem.cmem.domain_constants.copy() if mem else [], + parsing_schema=mem.cmem.parsing_schema.copy() if mem else [], + reusable_results=mem.cmem.reusable_results.copy() if mem else [], + ) + # Persist episode at deepest common folder when memory is enabled if mem is not None: common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) @@ -969,8 +1040,13 @@ async def _refine_scopes( common_dir, artifacts={"scopes": scope_desc}, ) + return final_scopes, context_memory + + # No memory enabled: return scopes with ContextMemory containing topics only + if scope_topics: + return final_scopes, context_memory + return final_scopes, None - return final_scopes finally: await cleanup_mcp_contexts(contexts) @@ -981,7 +1057,7 @@ async def aforward( is_local: bool = False, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[ScopeResult], ContextMap | None]: + ) -> tuple[list[ScopeResult], "ContextMemory | None"]: """Resolve scopes in the repository for the given MR. Args: @@ -992,7 +1068,7 @@ async def aforward( review_context: Review context with inherited memory Returns: - Tuple of (list of ScopeResult, final context map or None) + Tuple of (list of ScopeResult, final context memory or None) """ excluded_dirs = self._settings.excluded_directories reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] @@ -1007,9 +1083,11 @@ async def aforward( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, has_changes=True, changed_files=reviewable_files, reason="Scope identification disabled", + description="Repository root", ) fallback.skills = collect_skills(repo_path, ".") - return [fallback], review_context.memory if review_context else None + root_topic = fallback.topic(mr.repo_full_name) + return [fallback], ContextMemory(topics=[root_topic]) try: await self._ensure_repo(mr, repo_path, is_local) @@ -1030,7 +1108,7 @@ async def aforward( if orphans: logger.info("Deterministic identification produced %d orphan(s)", len(orphans)) - scopes = await self._refine_scopes( + scopes, context_memory = await self._refine_scopes( scopes, orphans, mr, repo_path, review_context, run_id ) # Log final scopes for visibility @@ -1039,15 +1117,18 @@ async def aforward( for s in scopes ) logger.info("Resolved %d scope(s) for %s:\n%s", len(scopes), mr.repo_slug, scope_summary) - return scopes, review_context.memory if review_context else None + return scopes, context_memory except Exception as e: logger.error("Scope resolution failed: %s", e, exc_info=True) - return [ScopeResult( + fallback = ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, has_changes=True, changed_files=reviewable_files, reason=f"Fallback due to error: {e}", - )], review_context.memory if review_context else None + description="Repository root", + ) + root_topic = fallback.topic(mr.repo_full_name) + return [fallback], ContextMemory(topics=[root_topic]) def forward( self, @@ -1056,7 +1137,7 @@ def forward( is_local: bool = False, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[ScopeResult], ContextMap | None]: + ) -> tuple[list[ScopeResult], ContextMemory | None]: """Resolve scopes (sync wrapper).""" return asyncio.run( self.aforward( diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 37928c6..92fa233 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -6,7 +6,7 @@ import dspy from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import ContextMap, Hippocampus +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.config import get_settings from codespy.config_memory import get_memory_store @@ -56,7 +56,9 @@ def forward( repo_slug: str, run_id: str | None = None, scopes: list["ScopeResult"] | None = None, - ) -> tuple[str, ContextMap | None]: + initial_memory: ContextMemory | None = None, + topic_ids: list[str] | None = None, + ) -> tuple[str, ContextMemory | None]: """Generate a PR summary. Args: @@ -68,14 +70,16 @@ def forward( repo_slug: Host-qualified repo slug for episode path run_id: Pipeline run identifier scopes: List of resolved scopes for per-scope episode persistence + initial_memory: Optional context memory from scope resolver + topic_ids: Optional list of topic IDs for auto-tagging Returns: - Tuple of (summary string, final context map or None) + Tuple of (summary string, final context memory or None) """ if not self._settings.is_signature_enabled("summary"): logger.debug("Skipping summary: disabled") - return mr_title or "No title", None + return mr_title or "No title", initial_memory summarizer = dspy.ChainOfThought(PRSummarySignature) logger.info("Generating PR summary...") @@ -92,6 +96,8 @@ def forward( question=question, task_name="summary", run_id=run_id, + initial_memory=initial_memory, + topic_ids=topic_ids, ) result = mem( mr_title=mr_title, @@ -113,8 +119,8 @@ def forward( ) logger.info(f"PR summary: {result.summary[:80]}...") - # Return final context map when memory is enabled - final_memory = mem.cmap.model_copy(deep=True) if mem else None + # Return final context memory when memory is enabled + final_memory = mem.cmem.model_copy(deep=True) if mem else initial_memory # Persist episode at each scope location if memory is enabled and scopes are provided if mem is not None and scopes: diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index fd7ba22..442b3d0 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,9 +8,9 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker -from codespy.agents.memory.hippocampus import Hippocampus -from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult +from codespy.tools.git.models import MergeRequest from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -242,7 +242,8 @@ async def aforward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for supply chain security vulnerabilities and return issues. For each scope, filesystem/parser tools are created rooted at @@ -256,9 +257,10 @@ async def aforward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run (see ``Hippocampus.run_id``) review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ # Check if supply chain signature is enabled if not self._settings.is_signature_enabled("supply_chain"): @@ -271,7 +273,7 @@ async def aforward( return [], review_context.memory if review_context else None all_issues: list[Issue] = [] - scope_memories: list[ContextMap] = [] + scope_memories: list[ContextMemory] = [] supply_chain_max_iters = self._settings.get_max_iters("supply_chain") # Create OSV tools once (shared across scopes, no filesystem root) @@ -331,6 +333,7 @@ async def aforward( f"review supply chain of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" ) if review_context else None + topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( supply_chain_agent, budget=self._settings.get_memory_budget("supply_chain"), @@ -341,6 +344,7 @@ async def aforward( task_name="supply_chain", run_id=run_id, initial_memory=review_context.memory if review_context else None, + topic_ids=topic_ids, ) result = await mem.aforward( manifest_path=manifest_path, @@ -357,9 +361,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) - # Collect scope's context map - if mem: - scope_memories.append(mem.cmap.model_copy(deep=True)) + # Collect scope's context memory + if mem: + scope_memories.append(mem.cmem.model_copy(deep=True)) else: result = await supply_chain_agent.acall( manifest_path=manifest_path, @@ -383,9 +387,9 @@ async def aforward( await cleanup_mcp_contexts(osv_contexts) logger.info(f"Security audit found {len(all_issues)} issues") - # Merge all scope context maps into one module-level map + # Merge all scope context memories into one module-level memory merged_memory = ( - ContextMap.merge(*scope_memories) + ContextMemory.merge(*scope_memories) if scope_memories else (review_context.memory if review_context else None) ) @@ -397,7 +401,8 @@ def forward( repo_path: Path, run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], ContextMap | None]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for supply chain security vulnerabilities (sync wrapper). Args: @@ -406,8 +411,9 @@ def forward( run_id: Identifier of the pipeline run, shared across all agents invoked within the same review run review_context: ReviewContext containing PR identity and inherited memory + mr: Optional merge request for topic ID computation Returns: - Tuple of (list of issues, merged context map or None) + Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context)) + return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 874e54c..e13fd2f 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -13,7 +13,7 @@ from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff from codespy.tools.git.patch_utils import compact_patches -from codespy.agents.memory.hippocampus import ContextMap +from codespy.agents.memory.hippocampus import ContextMemory from codespy.agents.reviewer.models import ( Issue, PRContext, @@ -94,7 +94,8 @@ async def _run_review_modules( module_names: list[str], run_id: str | None = None, review_context: ReviewContext | None = None, - ) -> tuple[list[Issue], dict[str, ContextMap | None]]: + mr: MergeRequest | None = None, + ) -> tuple[list[Issue], dict[str, ContextMemory | None]]: """Run review modules concurrently in a single event loop. Uses asyncio.gather instead of dspy.Parallel to avoid the @@ -110,25 +111,39 @@ async def _run_review_modules( review_context: ReviewContext for Hippocampus question and memory inheritance Returns: - Tuple of (aggregated list of issues, dict of module_name -> context_map) + Tuple of (aggregated list of issues, dict of module_name -> context_memory) """ + # Compute all scope topic IDs for summary/auditor modules + all_scope_topic_ids: list[str] = [] + if mr: + all_scope_topic_ids = [s.topic(mr.repo_full_name).id for s in scopes] + tasks = [ - self.code_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), - self.doc_reviewer.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), - self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path, run_id=run_id, review_context=review_context), + self.code_reviewer.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + review_context=review_context, mr=mr + ), + self.doc_reviewer.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + review_context=review_context, mr=mr + ), + self.supply_chain_auditor.aforward( + scopes=scopes, repo_path=repo_path, run_id=run_id, + review_context=review_context, mr=mr + ), ] results = await asyncio.gather(*tasks, return_exceptions=True) all_issues: list[Issue] = [] - context_maps: dict[str, ContextMap | None] = {} + context_memories: dict[str, ContextMemory | None] = {} for i, result in enumerate(results): if isinstance(result, Exception): logger.error(f"{module_names[i]} failed: {result}", exc_info=result) elif result is not None: - issues, ctx_map = result + issues, ctx_mem = result all_issues.extend(issues) - context_maps[module_names[i]] = ctx_map - return all_issues, context_maps + context_memories[module_names[i]] = ctx_mem + return all_issues, context_memories def _build_local_mr(self, config: LocalReviewConfig) -> MergeRequest: """Build a MergeRequest from local git changes. @@ -189,7 +204,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: summary=mr.title, # Use title as placeholder since summary hasn't run ) review_ctx = ReviewContext(pr_context=pr_context, memory=None) - scopes, _ = self.scope_resolver( + scopes, initial_memory = self.scope_resolver( mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_ctx ) for scope in scopes: @@ -210,6 +225,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: patches = build_patches(mr.changed_files) compact_patches(scopes, repo_path) # Step 2: Run Summarizer (now receives scopes for per-scope episode persistence) + # Compute all scope topic IDs for summarizer + all_scope_topic_ids = [s.topic(mr.repo_full_name).id for s in scopes] pr_summary, summarizer_memory = self.summarizer( mr_title=mr.title, mr_description=mr.body or "No description provided.", @@ -219,6 +236,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: repo_slug=mr.repo_slug, run_id=run_id, scopes=scopes, + initial_memory=initial_memory, + topic_ids=all_scope_topic_ids, ) # Enrich review_ctx with actual summary and memory from summarizer pr_context.summary = pr_summary @@ -227,12 +246,12 @@ def forward(self, config: ReviewConfig) -> ReviewResult: module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") all_issues, parallel_memories = asyncio.run( - self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, review_context=review_ctx) + self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, review_context=review_ctx, mr=mr) ) logger.info(f"Found {len(all_issues)} issues") - # Merge parallel context maps for Auditor - maps_to_merge = [m for m in parallel_memories.values() if m is not None] - merged_memory = ContextMap.merge(*maps_to_merge) if maps_to_merge else summarizer_memory + # Merge parallel context memories for Auditor + memories_to_merge = [m for m in parallel_memories.values() if m is not None] + merged_memory = ContextMemory.merge(*memories_to_merge) if memories_to_merge else summarizer_memory review_ctx = ReviewContext(pr_context=pr_context, memory=merged_memory) # Step 4: Run Audit (inherits merged memory from parallel modules) scoped_files = self._collect_scoped_files(scopes) @@ -246,6 +265,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: all_issues=all_issues, run_id=run_id, scopes=scopes, + topic_ids=all_scope_topic_ids, ) # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() diff --git a/src/codespy/config.py b/src/codespy/config.py index 0e88a2f..78a13e8 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -260,27 +260,27 @@ def get_memory_max_reflects(self, signature_name: str) -> int | None: else self.memory.default_max_reflects ) - def get_memory_max_context_map_tokens(self, signature_name: str) -> int: - """Get max_context_map_tokens for a signature's memory (signature-specific or default). + def get_memory_max_context_memory_tokens(self, signature_name: str) -> int: + """Get max_context_memory_tokens for a signature's memory (signature-specific or default). - Bounds the rendered ContextMap — the persisted artifact that is prepended + Bounds the rendered ContextMemory — the persisted artifact that is prepended to every predictor of the wrapped agent, and therefore re-sent on every ReAct iteration. """ config = self.get_signature_config(signature_name).memory return ( - config.max_context_map_tokens - if config.max_context_map_tokens is not None - else self.memory.default_max_context_map_tokens + config.max_context_memory_tokens + if config.max_context_memory_tokens is not None + else self.memory.default_max_context_memory_tokens ) def get_memory_max_context_item_tokens(self, signature_name: str) -> int: """Get max_context_item_tokens for a signature's memory (signature-specific or default). - Bounds a *single* context-map item. Handed to the Distiller and the + Bounds a *single* context-memory item. Handed to the Distiller and the Cartographer as a prompt input so they keep each item compact instead of - spending the whole map budget on one verbose entry. Soft limit — the hard, - map-wide ceiling is ``get_memory_max_context_map_tokens``. + spending the whole memory budget on one verbose entry. Soft limit — the hard, + memory-wide ceiling is ``get_memory_max_context_memory_tokens``. """ config = self.get_signature_config(signature_name).memory return ( @@ -331,7 +331,7 @@ def get_memory_budget(self, signature_name: str) -> "MemoryBudget": from codespy.agents.memory.hippocampus.budget import MemoryBudget return MemoryBudget( - max_context_map_tokens=self.get_memory_max_context_map_tokens(signature_name), + max_context_memory_tokens=self.get_memory_max_context_memory_tokens(signature_name), max_context_item_tokens=self.get_memory_max_context_item_tokens(signature_name), max_trajectory_tokens=self.get_memory_max_trajectory_tokens(signature_name), max_question_tokens=self.get_memory_max_question_tokens(signature_name), diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index 674a506..b03f91b 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -26,7 +26,7 @@ class MemorySignatureConfig(BaseModel): enabled: bool | None = None # _MEMORY_ENABLED max_reflects: int | None = None # _MEMORY_MAX_REFLECTS - max_context_map_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MAP_TOKENS + max_context_memory_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_item_tokens: int | None = None # _MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: int | None = None # _MEMORY_MAX_QUESTION_TOKENS @@ -94,7 +94,7 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - ``CODE_REVIEW_MAX_ITERS`` -> signatures.code_review.max_iters - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - - ``SCOPE_MEMORY_MAX_CONTEXT_MAP_TOKENS`` -> signatures.scope.memory.max_context_map_tokens + - ``SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS`` -> signatures.scope.memory.max_context_memory_tokens - ``SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS`` -> signatures.scope.memory.max_context_item_tokens Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index df5b38b..eb5f7cf 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -77,20 +77,20 @@ class MemoryConfig(BaseModel): default_enabled: bool = False # MEMORY_DEFAULT_ENABLED default_max_reflects: int = Field(default=0) # MEMORY_DEFAULT_MAX_REFLECTS - # Ceiling on the rendered ContextMap. This is the *persisted* artifact and it + # Ceiling on the rendered ContextMemory. This is the *persisted* artifact and it # is prepended to every predictor of the wrapped agent, so it is re-sent on # every ReAct iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. - # Approximate item capacity is default_max_context_map_tokens divided by + # Approximate item capacity is default_max_context_memory_tokens divided by # default_max_context_item_tokens (3072 / 240 ~= 12 items). - # MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS - default_max_context_map_tokens: int = Field(default=3072) + # MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS + default_max_context_memory_tokens: int = Field(default=3072) # Per-item ceiling handed to the Distiller/Cartographer as a prompt input, so - # they keep each context-map item compact instead of spending the whole map + # they keep each context-memory item compact instead of spending the whole memory # budget on one verbose entry. Soft limit: it is expressed to the LLM rather # than enforced in code (truncating an item could corrupt an exact constant). - # The hard, map-wide limit is default_max_context_map_tokens, enforced by the + # The hard, memory-wide limit is default_max_context_memory_tokens, enforced by the # Evictor. MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS default_max_context_item_tokens: int = Field(default=240) @@ -131,7 +131,7 @@ class MemoryConfig(BaseModel): "S3_ENDPOINT_URL": "s3_endpoint_url", "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", - "DEFAULT_MAX_CONTEXT_MAP_TOKENS": "default_max_context_map_tokens", + "DEFAULT_MAX_CONTEXT_MEMORY_TOKENS": "default_max_context_memory_tokens", "DEFAULT_MAX_CONTEXT_ITEM_TOKENS": "default_max_context_item_tokens", "DEFAULT_MAX_TRAJECTORY_TOKENS": "default_max_trajectory_tokens", "DEFAULT_MAX_QUESTION_TOKENS": "default_max_question_tokens", @@ -167,7 +167,7 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: MEMORY_BACKEND=s3 -> memory.backend MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled - MEMORY_DEFAULT_MAX_CONTEXT_MAP_TOKENS=512 -> memory.default_max_context_map_tokens + MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS=512 -> memory.default_max_context_memory_tokens Reflection module overrides use a second level of nesting:: diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py new file mode 100644 index 0000000..7ab8a63 --- /dev/null +++ b/tests/test_context_memory.py @@ -0,0 +1,480 @@ +"""Tests for ContextMemory with Topics.""" + +from pathlib import Path +from unittest.mock import patch + +import pytest + +from codespy.agents.memory.hippocampus import ( + ContextMemory, + Item, + OpType, + Operation, + Topic, + compute_common_ancestor_topic_id, + make_topic_id, +) +from codespy.agents.reviewer.modules.manifest_parser import extract_package_name + + +class TestMakeTopicId: + """Tests for make_topic_id function.""" + + def test_package_name_contains_repo(self): + """Path 1: package_name contains owner/repo -> use from owner/repo onwards.""" + package_name = "github.com/owner/repo/packages/auth" + result = make_topic_id("owner/repo", "packages/auth", package_name) + assert result == "owner/repo/packages/auth" + + def test_package_name_without_repo(self): + """Path 2: package_name provided but doesn't contain repo -> prepend repo.""" + package_name = "@myorg/auth-service" + result = make_topic_id("owner/repo", "packages/auth", package_name) + assert result == "owner/repo/@myorg/auth-service" + + def test_subroot_fallback(self): + """Path 3: no package_name -> use subroot.""" + result = make_topic_id("owner/repo", "packages/auth", None) + assert result == "owner/repo/packages/auth" + + def test_root_scope(self): + """Root scope (subroot='.') returns just repo_full_name.""" + result = make_topic_id("owner/repo", ".", None) + assert result == "owner/repo" + + def test_empty_subroot(self): + """Empty subroot returns just repo_full_name.""" + result = make_topic_id("owner/repo", "", None) + assert result == "owner/repo" + + def test_strips_leading_slashes(self): + """Leading slashes in subroot are stripped.""" + result = make_topic_id("owner/repo", "/packages/auth/", None) + assert result == "owner/repo/packages/auth" + + +class TestComputeCommonAncestorTopicId: + """Tests for compute_common_ancestor_topic_id function.""" + + def test_single_scope_returns_none(self): + """Single scope returns None (no common ancestor needed).""" + result = compute_common_ancestor_topic_id("owner/repo", ["packages/auth"]) + assert result is None + + def test_empty_list_returns_none(self): + """Empty list returns None.""" + result = compute_common_ancestor_topic_id("owner/repo", []) + assert result is None + + def test_two_scopes_with_common_ancestor(self): + """Two scopes sharing common ancestor.""" + subroots = ["packages/auth", "packages/api"] + result = compute_common_ancestor_topic_id("owner/repo", subroots) + assert result == "owner/repo/packages" + + def test_three_scopes_with_common_ancestor(self): + """Three scopes with nested common ancestor.""" + subroots = ["packages/auth/src", "packages/auth/tests", "packages/api"] + result = compute_common_ancestor_topic_id("owner/repo", subroots) + assert result == "owner/repo/packages" + + def test_disjoint_scopes_returns_root(self): + """Disjoint scopes with no common ancestor return root.""" + subroots = ["frontend", "backend"] + result = compute_common_ancestor_topic_id("owner/repo", subroots) + assert result == "owner/repo" + + def test_one_root_scope(self): + """Mix of root and nested scopes returns root.""" + subroots = [".", "packages/auth"] + result = compute_common_ancestor_topic_id("owner/repo", subroots) + assert result == "owner/repo" + + def test_identical_scopes(self): + """Identical scopes return that scope.""" + subroots = ["packages/auth", "packages/auth"] + result = compute_common_ancestor_topic_id("owner/repo", subroots) + assert result == "owner/repo/packages/auth" + + +class TestExtractPackageName: + """Tests for extract_package_name function.""" + + def test_extract_from_package_json(self, tmp_path: Path): + """Extract name from package.json.""" + manifest = tmp_path / "package.json" + manifest.write_text('{"name": "my-package", "version": "1.0.0"}') + result = extract_package_name("package.json", tmp_path) + assert result == "my-package" + + def test_extract_from_composer_json(self, tmp_path: Path): + """Extract name from composer.json.""" + manifest = tmp_path / "composer.json" + manifest.write_text('{"name": "vendor/package", "version": "1.0.0"}') + result = extract_package_name("composer.json", tmp_path) + assert result == "vendor/package" + + def test_extract_from_go_mod(self, tmp_path: Path): + """Extract module from go.mod.""" + manifest = tmp_path / "go.mod" + manifest.write_text("module github.com/owner/repo\n\ngo 1.21\n") + result = extract_package_name("go.mod", tmp_path) + assert result == "github.com/owner/repo" + + def test_extract_from_cargo_toml(self, tmp_path: Path): + """Extract name from Cargo.toml.""" + manifest = tmp_path / "Cargo.toml" + manifest.write_text('[package]\nname = "my-crate"\nversion = "1.0.0"') + result = extract_package_name("Cargo.toml", tmp_path) + assert result == "my-crate" + + def test_extract_from_pubspec_yaml(self, tmp_path: Path): + """Extract name from pubspec.yaml.""" + manifest = tmp_path / "pubspec.yaml" + manifest.write_text("name: my_app\ndescription: A Flutter app\n") + result = extract_package_name("pubspec.yaml", tmp_path) + assert result == "my_app" + + def test_extract_from_chart_yaml(self, tmp_path: Path): + """Extract name from Chart.yaml.""" + manifest = tmp_path / "Chart.yaml" + manifest.write_text("apiVersion: v2\nname: my-chart\nversion: 1.0.0\n") + result = extract_package_name("Chart.yaml", tmp_path) + assert result == "my-chart" + + def test_extract_from_setup_cfg(self, tmp_path: Path): + """Extract name from setup.cfg.""" + manifest = tmp_path / "setup.cfg" + manifest.write_text("[metadata]\nname = my-package\nversion = 1.0.0\n") + result = extract_package_name("setup.cfg", tmp_path) + assert result == "my-package" + + def test_pyproject_toml_project_name(self, tmp_path: Path): + """Extract name from pyproject.toml [project].""" + manifest = tmp_path / "pyproject.toml" + manifest.write_text('[project]\nname = "my-project"\nversion = "1.0.0"\n') + result = extract_package_name("pyproject.toml", tmp_path) + assert result == "my-project" + + def test_pyproject_toml_poetry_name(self, tmp_path: Path): + """Extract name from pyproject.toml [tool.poetry].""" + manifest = tmp_path / "pyproject.toml" + manifest.write_text('[tool.poetry]\nname = "poetry-project"\nversion = "1.0.0"\n') + result = extract_package_name("pyproject.toml", tmp_path) + assert result == "poetry-project" + + def test_missing_manifest_returns_none(self, tmp_path: Path): + """Missing manifest file returns None.""" + result = extract_package_name("package.json", tmp_path) + assert result is None + + def test_malformed_json_returns_none(self, tmp_path: Path): + """Malformed JSON returns None.""" + manifest = tmp_path / "package.json" + manifest.write_text("not valid json") + result = extract_package_name("package.json", tmp_path) + assert result is None + + def test_missing_name_field_returns_none(self, tmp_path: Path): + """JSON without name field returns None.""" + manifest = tmp_path / "package.json" + manifest.write_text('{"version": "1.0.0"}') + result = extract_package_name("package.json", tmp_path) + assert result is None + + def test_gradle_from_settings_gradle(self, tmp_path: Path): + """Extract project name from settings.gradle.""" + manifest = tmp_path / "build.gradle" + manifest.write_text("") + settings = tmp_path / "settings.gradle" + settings.write_text("rootProject.name = 'my-project'") + result = extract_package_name("build.gradle", tmp_path) + assert result == "my-project" + + +class TestContextMemoryRender: + """Tests for ContextMemory.render() method.""" + + def test_topic_grouped_format(self): + """Render produces topic-grouped format.""" + topics = [ + Topic(id="owner/repo/auth", description="Auth service"), + Topic(id="owner/repo/api", description="API gateway"), + ] + items = [ + Item(id="cu-abc", content="Auth uses JWT", topic_ids=["owner/repo/auth"]), + Item(id="cu-def", content="API uses rate limiting", topic_ids=["owner/repo/api"]), + ] + memory = ContextMemory( + topics=topics, + context_understanding=items, + ) + result = memory.render() + + assert "## TOPIC: owner/repo/auth (Auth service)" in result + assert "## TOPIC: owner/repo/api (API gateway)" in result + assert "[cu-abc] Auth uses JWT" in result + assert "[cu-def] API uses rate limiting" in result + + def test_shared_group_for_multiple_topic_ids(self): + """Items with 2+ topic_ids go to SHARED group.""" + topics = [ + Topic(id="owner/repo/auth", description="Auth service"), + Topic(id="owner/repo/api", description="API gateway"), + ] + items = [ + Item(id="cu-shared", content="Shared context", topic_ids=["owner/repo/auth", "owner/repo/api"]), + ] + memory = ContextMemory( + topics=topics, + context_understanding=items, + ) + result = memory.render() + + assert "## SHARED" in result + assert "[cu-shared] Shared context" in result + + def test_shared_group_for_empty_topic_ids(self): + """Items with empty topic_ids go to SHARED group.""" + topics = [Topic(id="owner/repo/auth", description="Auth service")] + items = [ + Item(id="cu-empty", content="No topic", topic_ids=[]), + ] + memory = ContextMemory( + topics=topics, + context_understanding=items, + ) + result = memory.render() + + assert "## SHARED" in result + assert "[cu-empty] No topic" in result + + def test_shared_group_for_unknown_topic_id(self): + """Items with unknown topic_id go to SHARED group.""" + topics = [Topic(id="owner/repo/auth", description="Auth service")] + items = [ + Item(id="cu-unknown", content="Unknown topic", topic_ids=["nonexistent"]), + ] + memory = ContextMemory( + topics=topics, + context_understanding=items, + ) + result = memory.render() + + assert "## SHARED" in result + assert "[cu-unknown] Unknown topic" in result + + def test_empty_memory_returns_empty_string(self): + """Completely empty memory returns empty string.""" + memory = ContextMemory() + result = memory.render() + assert result == "" + + def test_topic_with_no_items_is_hidden(self): + """Topics with no items are not rendered.""" + topics = [ + Topic(id="owner/repo/auth", description="Auth service"), + Topic(id="owner/repo/empty", description="Empty scope"), + ] + items = [ + Item(id="cu-abc", content="Only auth has items", topic_ids=["owner/repo/auth"]), + ] + memory = ContextMemory( + topics=topics, + context_understanding=items, + ) + result = memory.render() + + assert "owner/repo/auth" in result + assert "owner/repo/empty" not in result + + def test_section_headers_in_render(self): + """Render includes section headers for non-empty sections.""" + topics = [Topic(id="owner/repo/auth", description="Auth service")] + memory = ContextMemory( + topics=topics, + context_roadmap=[Item(id="cr-1", content="Roadmap item", topic_ids=["owner/repo/auth"])], + context_understanding=[Item(id="cu-1", content="Understanding item", topic_ids=["owner/repo/auth"])], + ) + result = memory.render() + + assert "### CONTEXT ROADMAP" in result + assert "### CONTEXT UNDERSTANDING" in result + + def test_shared_appears_first(self): + """SHARED group appears before topic groups.""" + topics = [Topic(id="owner/repo/auth", description="Auth service")] + memory = ContextMemory( + topics=topics, + context_understanding=[ + Item(id="cu-1", content="Topic item", topic_ids=["owner/repo/auth"]), + Item(id="cu-2", content="Shared item", topic_ids=[]), + ], + ) + result = memory.render() + + shared_pos = result.find("## SHARED") + topic_pos = result.find("## TOPIC") + assert shared_pos < topic_pos + + +class TestContextMemoryApply: + """Tests for ContextMemory.apply() method.""" + + def test_add_operation_with_topic_ids(self): + """ADD operation assigns topic_ids to new items.""" + memory = ContextMemory(topics=[Topic(id="t1", description="Test")]) + ops = [Operation(type=OpType.ADD, section="context_understanding", content="New item")] + new_memory, new_ids = memory.apply(ops, topic_ids=["t1"]) + + assert len(new_ids) == 1 + item = new_memory.context_understanding[0] + assert item.topic_ids == ["t1"] + + def test_replace_preserves_existing_topic_ids(self): + """REPLACE operation preserves existing topic_ids.""" + memory = ContextMemory( + context_understanding=[ + Item(id="cu-abc", content="Original", topic_ids=["t1"]), + ], + ) + ops = [Operation(type=OpType.REPLACE, item_id="cu-abc", content="Replaced")] + new_memory, _ = memory.apply(ops, topic_ids=["t2"]) + + item = new_memory.context_understanding[0] + assert item.content == "Replaced" + assert item.topic_ids == ["t1"] # Preserved, not overwritten + + def test_add_without_topic_ids(self): + """ADD without topic_ids creates item with empty topic_ids.""" + memory = ContextMemory() + ops = [Operation(type=OpType.ADD, section="context_understanding", content="New item")] + new_memory, _ = memory.apply(ops) + + item = new_memory.context_understanding[0] + assert item.topic_ids == [] + + def test_delete_removes_item(self): + """DELETE operation removes item.""" + memory = ContextMemory( + context_understanding=[ + Item(id="cu-abc", content="To delete", topic_ids=["t1"]), + ], + ) + ops = [Operation(type=OpType.DELETE, item_id="cu-abc")] + new_memory, _ = memory.apply(ops) + + assert len(new_memory.context_understanding) == 0 + + +class TestContextMemoryMerge: + """Tests for ContextMemory.merge() method.""" + + def test_merge_deduplicates_topics(self): + """Merge deduplicates topics by ID.""" + mem1 = ContextMemory(topics=[Topic(id="t1", description="First")]) + mem2 = ContextMemory(topics=[Topic(id="t1", description="Second")]) + merged = ContextMemory.merge(mem1, mem2) + + assert len(merged.topics) == 1 + + def test_merge_later_description_wins(self): + """Later non-empty description wins in topic merge.""" + mem1 = ContextMemory(topics=[Topic(id="t1", description="")]) + mem2 = ContextMemory(topics=[Topic(id="t1", description="Better description")]) + merged = ContextMemory.merge(mem1, mem2) + + assert merged.topics[0].description == "Better description" + + def test_merge_items_by_id(self): + """Merge replaces items with same ID (later wins).""" + mem1 = ContextMemory( + context_understanding=[Item(id="cu-abc", content="First", topic_ids=["t1"])], + ) + mem2 = ContextMemory( + context_understanding=[Item(id="cu-abc", content="Second", topic_ids=["t2"])], + ) + merged = ContextMemory.merge(mem1, mem2) + + assert len(merged.context_understanding) == 1 + assert merged.context_understanding[0].content == "Second" + + def test_merge_multiple_memories(self): + """Merge can handle multiple memories.""" + mem1 = ContextMemory( + topics=[Topic(id="t1", description="T1")], + context_understanding=[Item(id="cu-1", content="Item 1", topic_ids=["t1"])], + ) + mem2 = ContextMemory( + topics=[Topic(id="t2", description="T2")], + context_understanding=[Item(id="cu-2", content="Item 2", topic_ids=["t2"])], + ) + mem3 = ContextMemory( + topics=[Topic(id="t3", description="T3")], + domain_constants=[Item(id="dc-1", content="Constant", topic_ids=["t3"])], + ) + merged = ContextMemory.merge(mem1, mem2, mem3) + + assert len(merged.topics) == 3 + assert len(merged.context_understanding) == 2 + assert len(merged.domain_constants) == 1 + + +class TestScopeResultTopicHelper: + """Tests for ScopeResult.topic() helper method.""" + + def test_topic_with_package_manifest(self): + """Topic uses package_name from manifest when available.""" + from codespy.agents.reviewer.models import PackageManifest, ScopeResult, ScopeType + + scope = ScopeResult( + repo="owner/repo", + subroot="packages/auth", + scope_type=ScopeType.SERVICE, + reason="test", + description="Auth service", + package_manifest=PackageManifest( + manifest_path="packages/auth/package.json", + package_manager="npm", + package_name="@myorg/auth", + ), + ) + topic = scope.topic("owner/repo") + + assert topic.id == "owner/repo/@myorg/auth" + assert topic.description == "Auth service" + + def test_topic_without_package_manifest(self): + """Topic uses subroot when no package manifest.""" + from codespy.agents.reviewer.models import ScopeResult, ScopeType + + scope = ScopeResult( + repo="owner/repo", + subroot="services/api", + scope_type=ScopeType.SERVICE, + reason="test", + description="API service", + ) + topic = scope.topic("owner/repo") + + assert topic.id == "owner/repo/services/api" + assert topic.description == "API service" + + def test_topic_for_root_scope(self): + """Root scope topic uses just repo_full_name.""" + from codespy.agents.reviewer.models import ScopeResult, ScopeType + + scope = ScopeResult( + repo="owner/repo", + subroot=".", + scope_type=ScopeType.APPLICATION, + reason="test", + description="Repository root", + ) + topic = scope.topic("owner/repo") + + assert topic.id == "owner/repo" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From a47a5ccc2c49f04620794c279bbb61030368772f Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 16 Aug 2026 20:45:39 +0200 Subject: [PATCH 60/79] wip --- .../memory/hippocampus/context_memory.py | 4 + .../reviewer/modules/manifest_parser.py | 577 ++++++++++++++++++ .../agents/reviewer/modules/scope_resolver.py | 53 +- tests/test_context_memory.py | 25 + 4 files changed, 654 insertions(+), 5 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 72a9bfd..478f745 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -57,6 +57,10 @@ class Topic(BaseModel): id: str = Field(description="Topic identifier (e.g., 'owner/repo/package-name')") description: str = Field(description="Description of this topic's role") + dependencies: list[str] = Field( + default_factory=list, + description="Topic IDs of this topic's dependencies" + ) class Item(BaseModel): diff --git a/src/codespy/agents/reviewer/modules/manifest_parser.py b/src/codespy/agents/reviewer/modules/manifest_parser.py index 12d6745..c36b904 100644 --- a/src/codespy/agents/reviewer/modules/manifest_parser.py +++ b/src/codespy/agents/reviewer/modules/manifest_parser.py @@ -8,6 +8,583 @@ from pathlib import Path from xml.etree import ElementTree as ET +# Mapping of package manager to ecosystem name +PACKAGE_MANAGER_TO_ECOSYSTEM: dict[str, str] = { + "npm": "npm", + "go": "Go", + "pip": "PyPI", + "cargo": "crates.io", + "maven": "Maven", + "gradle": "Maven", + "sbt": "Maven", + "composer": "Packagist", + "bundler": "RubyGems", + "dotnet": "NuGet", + "swift": "SwiftURL", + "pub": "Pub", + "mix": "Hex", + "helm": "Helm", + "clojure": "Clojure", + "leiningen": "Clojure", + "stack": "Hackage", + "cabal": "Hackage", + "dune": "opam", + "zig": "Zig", + "cpan": "CPAN", + "r": "CRAN", +} + +# Git hosts for repo inference +_GIT_HOSTS = ("github.com/", "gitlab.com/", "bitbucket.org/") + + +def _infer_repo_from_url(url: str) -> str | None: + """Extract owner/repo from git URL (e.g., https://github.com/owner/repo.git).""" + for host in _GIT_HOSTS: + for scheme in (f"https://{host}", f"http://{host}", f"git@{host.rstrip('/')}:"): + if url.startswith(scheme): + path = url[len(scheme):].rstrip("/").removesuffix(".git") + parts = path.split("/") + if len(parts) >= 2: + return f"{parts[0]}/{parts[1]}" + return None + + +def _infer_repo_from_name(name: str) -> str | None: + """Extract owner/repo from a name with git host prefix (e.g., Go module).""" + for host in _GIT_HOSTS: + if name.startswith(host): + parts = name[len(host):].split("/") + if len(parts) >= 2: + return f"{parts[0]}/{parts[1]}" + return None + + +def _infer_repo_from_path(path: str) -> str | None: + """Extract owner/repo from a file path containing a git host (vendored deps).""" + for host in _GIT_HOSTS: + idx = path.find(host) + if idx >= 0: + remainder = path[idx + len(host):] + parts = remainder.split("/") + if len(parts) >= 2: + return f"{parts[0]}/{parts[1]}" + return None + + +def infer_repo_from_name(name: str) -> str | None: + """Public wrapper for _infer_repo_from_name.""" + return _infer_repo_from_name(name) + + +def extract_dependencies(manifest_path: str, repo_path: Path) -> tuple[list[str], dict[str, str]]: + """Extract production dependency names and inferred source repos from manifest. + + Args: + manifest_path: Relative path to manifest file from repo root + repo_path: Path to the repository root + + Returns: + Tuple of: + - dependency_names: list of all production dep names + - dependency_repos: dict mapping dep name -> owner/repo for identifiable deps + """ + full_path = repo_path / manifest_path + if not full_path.exists(): + return [], {} + + filename = Path(manifest_path).name + + try: + if filename == "package.json": + return _extract_deps_from_package_json(full_path) + elif filename == "go.mod": + return _extract_deps_from_go_mod(full_path) + elif filename == "pyproject.toml": + return _extract_deps_from_pyproject_toml(full_path) + elif filename == "Cargo.toml": + return _extract_deps_from_cargo_toml(full_path) + elif filename == "pom.xml": + return _extract_deps_from_pom_xml(full_path) + elif filename == "composer.json": + return _extract_deps_from_composer_json(full_path) + elif filename == "pubspec.yaml": + return _extract_deps_from_pubspec_yaml(full_path) + elif filename == "Gemfile": + return _extract_deps_from_gemfile(full_path) + elif filename == "mix.exs": + return _extract_deps_from_mix_exs(full_path) + elif filename.endswith(".csproj"): + return _extract_deps_from_csproj(full_path) + elif filename == "Package.swift": + return _extract_deps_from_swift_package(full_path) + elif filename in ("build.gradle", "build.gradle.kts"): + return _extract_deps_from_gradle(full_path) + elif filename == "setup.cfg": + return _extract_deps_from_setup_cfg(full_path) + except Exception: + return [], {} + + return [], {} + + +def _extract_deps_from_package_json(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from package.json (production only, skip dev/peer/optional).""" + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + deps = data.get("dependencies", {}) + if not isinstance(deps, dict): + return [], {} + + names = list(deps.keys()) + repos: dict[str, str] = {} + + for name, spec in deps.items(): + if isinstance(spec, str): + # Parse git URLs: "github:owner/repo" or "git+https://..." + if spec.startswith("github:"): + repo_path = spec[7:].split("#")[0] # Remove any #ref + if "/" in repo_path: + repos[name] = repo_path + elif spec.startswith("git+"): + inferred = _infer_repo_from_url(spec[4:]) + if inferred: + repos[name] = inferred + + return names, repos + except (json.JSONDecodeError, UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_go_mod(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from go.mod (filter // indirect lines).""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + names: list[str] = [] + repos: dict[str, str] = {} + + # Parse require block + in_require = False + for line in content.split("\n"): + line = line.strip() + if line.startswith("require ("): + in_require = True + continue + if in_require and line == ")": + in_require = False + continue + if not in_require and line.startswith("require "): + # Single-line require + parts = line[8:].strip().split() + if parts: + line = parts[0] + else: + continue + + if in_require or (line and not line.startswith("require ")): + # Skip indirect deps + if "// indirect" in line: + continue + # Extract module path + parts = line.split() + if parts: + module_path = parts[0] + names.append(module_path) + # Go modules always have host in path + inferred = _infer_repo_from_name(module_path) + if inferred: + repos[module_path] = inferred + + return names, repos + except (UnicodeDecodeError, OSError): + return [], {} + + +def _strip_pep508_extras(name: str) -> str: + """Strip extras and version specifiers from PEP 508 dependency name.""" + # Handle name[extra] -> name + if "[" in name: + name = name.split("[")[0] + # Handle version specifiers (>=, ==, ~=, etc.) + for op in (">=", "<=", ">", "<", "==", "!=", "~=", "==="): + if op in name: + name = name.split(op)[0].strip() + return name.strip() + + +def _extract_deps_from_pyproject_toml(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from pyproject.toml (PEP 508 or Poetry).""" + try: + import tomllib + + with open(path, "rb") as f: + data = tomllib.load(f) + + names: list[str] = [] + repos: dict[str, str] = {} + + # Try [project.dependencies] (PEP 508) + project = data.get("project") + if isinstance(project, dict): + deps = project.get("dependencies", []) + if isinstance(deps, list): + for dep in deps: + if isinstance(dep, str): + name = _strip_pep508_extras(dep) + if name and name != "python": + names.append(name) + + # Try [tool.poetry.dependencies] + tool = data.get("tool") + if isinstance(tool, dict): + poetry = tool.get("poetry") + if isinstance(poetry, dict): + poetry_deps = poetry.get("dependencies", {}) + if isinstance(poetry_deps, dict): + for name, spec in poetry_deps.items(): + if name == "python": + continue + names.append(name) + # Check for git or path source + if isinstance(spec, dict): + git_url = spec.get("git") + if git_url and isinstance(git_url, str): + inferred = _infer_repo_from_url(git_url) + if inferred: + repos[name] = inferred + path_val = spec.get("path") + if path_val and isinstance(path_val, str): + inferred = _infer_repo_from_path(path_val) + if inferred: + repos[name] = inferred + + return names, repos + except Exception: + return [], {} + + +def _extract_deps_from_cargo_toml(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from Cargo.toml (skip dev-dependencies, build-dependencies).""" + try: + import tomllib + + with open(path, "rb") as f: + data = tomllib.load(f) + + names: list[str] = [] + repos: dict[str, str] = {} + + deps = data.get("dependencies", {}) + if isinstance(deps, dict): + for name, spec in deps.items(): + names.append(name) + if isinstance(spec, dict): + git_url = spec.get("git") + if git_url and isinstance(git_url, str): + inferred = _infer_repo_from_url(git_url) + if inferred: + repos[name] = inferred + + return names, repos + except Exception: + return [], {} + + +def _extract_deps_from_pom_xml(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from pom.xml (skip test scope).""" + try: + tree = ET.parse(path) + root = tree.getroot() + + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + + names: list[str] = [] + + deps = root.find("m:dependencies", ns) + if deps is None: + deps = root.find("dependencies") + + if deps is not None: + for dep in deps.findall("m:dependency", ns) if deps else []: + scope = dep.find("m:scope", ns) + if scope is not None and scope.text == "test": + continue + group = dep.find("m:groupId", ns) + artifact = dep.find("m:artifactId", ns) + if group is not None and artifact is not None: + names.append(f"{group.text}:{artifact.text}") + + return names, {} + except ET.ParseError: + return [], {} + + +def _extract_deps_from_composer_json(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from composer.json (exclude php, ext-*).""" + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + + require = data.get("require", {}) + if not isinstance(require, dict): + return [], {} + + names = [name for name in require.keys() + if not name.startswith("php") and not name.startswith("ext-")] + + return names, {} + except (json.JSONDecodeError, UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_pubspec_yaml(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from pubspec.yaml (exclude flutter packages).""" + try: + import yaml + + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + deps = data.get("dependencies", {}) + if not isinstance(deps, dict): + return [], {} + + excluded = {"flutter", "flutter_test", "flutter_localizations"} + names: list[str] = [] + repos: dict[str, str] = {} + + for name, spec in deps.items(): + if name in excluded: + continue + names.append(name) + if isinstance(spec, dict): + git_url = spec.get("git") + if isinstance(git_url, str): + inferred = _infer_repo_from_url(git_url) + if inferred: + repos[name] = inferred + elif isinstance(git_url, dict): + url = git_url.get("url") + if url and isinstance(url, str): + inferred = _infer_repo_from_url(url) + if inferred: + repos[name] = inferred + + return names, repos + except Exception: + return [], {} + + +def _extract_deps_from_gemfile(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from Gemfile (skip dev/test groups).""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + names: list[str] = [] + repos: dict[str, str] = {} + + # Track if we're in a dev/test group + in_dev_group = False + group_depth = 0 + + for line in content.split("\n"): + line_stripped = line.strip() + + # Track group blocks + if line_stripped.startswith("group "): + if ":development" in line_stripped or ":test" in line_stripped: + in_dev_group = True + group_depth += 1 + continue + + if line_stripped == "end" and group_depth > 0: + group_depth -= 1 + if group_depth == 0: + in_dev_group = False + continue + + if in_dev_group: + continue + + # Parse gem lines + match = re.match(r"gem\s+['\"]([^'\"]+)['\"]", line_stripped) + if match: + name = match.group(1) + names.append(name) + + # Check for git or github option + git_match = re.search(r"git:\s*['\"]([^'\"]+)['\"]", line) + if git_match: + inferred = _infer_repo_from_url(git_match.group(1)) + if inferred: + repos[name] = inferred + + github_match = re.search(r"github:\s*['\"]([^'\"]+)['\"]", line) + if github_match: + gh_path = github_match.group(1) + repos[name] = gh_path if "/" in gh_path else f"{gh_path}/{gh_path}" + + return names, repos + except (UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_mix_exs(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from mix.exs (skip dev/test only).""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + names: list[str] = [] + repos: dict[str, str] = {} + + # Find deps function + deps_match = re.search(r"defp?\s+deps\s*do\s*\[(.*?)\]\s*end", content, re.DOTALL) + if not deps_match: + return [], {} + + deps_block = deps_match.group(1) + + # Parse each dep tuple + for dep_match in re.finditer(r"\{([^}]+)\}", deps_block): + dep_str = dep_match.group(1) + # Skip if only: :dev or only: :test + if "only: :dev" in dep_str or "only: :test" in dep_str: + continue + + # Extract name (first atom or string) + name_match = re.match(r":([a-z_][a-zA-Z0-9_]*)|\"([^\"]+)\"", dep_str.strip()) + if name_match: + name = name_match.group(1) or name_match.group(2) + if name: + names.append(name) + + # Check for github option + gh_match = re.search(r"github:\s*\"([^\"]+)\"", dep_str) + if gh_match: + gh_path = gh_match.group(1) + repos[name] = gh_path if "/" in gh_path else f"{gh_path}/{gh_path}" + + return names, repos + except (UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_csproj(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from .csproj (skip PrivateAssets=All).""" + try: + tree = ET.parse(path) + root = tree.getroot() + + ns = {"p": "http://schemas.microsoft.com/developer/msbuild/2003"} + + names: list[str] = [] + + for ref in root.findall(".//p:PackageReference", ns): + private = ref.get("PrivateAssets") + if private == "All": + continue + include = ref.get("Include") + if include: + names.append(include) + + # Also try without namespace + if not names: + for ref in root.findall(".//PackageReference"): + private = ref.get("PrivateAssets") + if private == "All": + continue + include = ref.get("Include") + if include: + names.append(include) + + return names, {} + except ET.ParseError: + return [], {} + + +def _extract_deps_from_swift_package(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from Package.swift.""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + names: list[str] = [] + repos: dict[str, str] = {} + + # Match .package(url: "...", ...) + for match in re.finditer(r'\.package\s*\([^)]*url:\s*["\']([^"\']+)["\']', content): + url = match.group(1) + inferred = _infer_repo_from_url(url) + if inferred: + # Use repo name as dep name for Swift + dep_name = inferred.split("/")[-1] + names.append(dep_name) + repos[dep_name] = inferred + else: + # Extract name from URL + parts = url.rstrip("/").split("/") + if parts: + name = parts[-1].removesuffix(".git") + names.append(name) + + return names, repos + except (UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_gradle(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from build.gradle/build.gradle.kts (skip test/debug).""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + names: list[str] = [] + + # Match implementation, api, compile dependencies + for match in re.finditer(r"(implementation|api|compile)\s*['\"]([^'\"]+)['\"]", content): + coord = match.group(2) + # Skip test/debug variants + if not coord.startswith("test") and not coord.startswith("debug"): + names.append(coord) + + # Match Kotlin DSL: implementation("...") + for match in re.finditer(r"(implementation|api)\s*\(\s*['\"]([^'\"]+)['\"]\s*\)", content): + coord = match.group(2) + if not coord.startswith("test") and not coord.startswith("debug"): + names.append(coord) + + return names, {} + except (UnicodeDecodeError, OSError): + return [], {} + + +def _extract_deps_from_setup_cfg(path: Path) -> tuple[list[str], dict[str, str]]: + """Extract deps from setup.cfg [options] install_requires.""" + try: + config = configparser.ConfigParser() + config.read(path, encoding="utf-8") + + names: list[str] = [] + + if config.has_option("options", "install_requires"): + deps_str = config.get("options", "install_requires") + for line in deps_str.strip().split("\n"): + line = line.strip() + if line and not line.startswith("#"): + name = _strip_pep508_extras(line) + if name: + names.append(name) + + return names, {} + except (configparser.Error, UnicodeDecodeError, OSError): + return [], {} + def extract_package_name(manifest_path: str, repo_path: Path) -> str | None: """Extract package name from manifest file. Returns None on failure. diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 49ee30a..f899ac5 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -31,7 +31,12 @@ from codespy.tools.git.client import get_client from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server -from codespy.agents.reviewer.modules.manifest_parser import extract_package_name +from codespy.agents.reviewer.modules.manifest_parser import ( + extract_package_name, + extract_dependencies, + PACKAGE_MANAGER_TO_ECOSYSTEM, + infer_repo_from_name, +) logger = logging.getLogger(__name__) @@ -919,7 +924,9 @@ async def _refine_scopes( Tuple of (list of ScopeResult with agent-resolved assignments, ContextMemory with topics and items from Hippocampus) """ - from codespy.agents.memory.hippocampus import ContextMemory, Topic, compute_common_ancestor_topic_id + from codespy.agents.memory.hippocampus import ( + ContextMemory, Topic, compute_common_ancestor_topic_id, make_topic_id, + ) # Build candidates string from already-resolved scopes candidates_str = "\n".join(self._format_candidate(s) for s in scopes) @@ -984,11 +991,47 @@ async def _refine_scopes( if scope.subroot in boundary_descriptions: scope.description = boundary_descriptions[scope.subroot] - # Build topics from final scopes + # Build topic IDs + internal lookup + internal_packages: dict[str, str] = {} + scope_topic_ids: dict[str, str] = {} + for scope in final_scopes: + pkg_name = scope.package_manifest.package_name if scope.package_manifest else None + tid = make_topic_id(mr.repo_full_name, scope.subroot, pkg_name) + scope_topic_ids[scope.subroot] = tid + if pkg_name: + internal_packages[pkg_name] = tid + + # Build Topics with resolved dependencies scope_topics: list[Topic] = [] for scope in final_scopes: - topic = scope.topic(mr.repo_full_name) - scope_topics.append(topic) + dep_topic_ids: list[str] = [] + if scope.package_manifest: + dep_names, dep_repos = extract_dependencies( + scope.package_manifest.manifest_path, repo_path + ) + ecosystem = PACKAGE_MANAGER_TO_ECOSYSTEM.get( + scope.package_manifest.package_manager, + scope.package_manifest.package_manager, + ) + for name in dep_names: + if name in internal_packages: + # Rule 1: internal scope match + dep_topic_ids.append(internal_packages[name]) + elif name in dep_repos: + # Rule 2: repo identifiable from source metadata + dep_topic_ids.append(make_topic_id(dep_repos[name], "", name)) + elif infer_repo_from_name(name): + # Rule 2: repo identifiable from dep name (Go modules) + dep_topic_ids.append(make_topic_id(infer_repo_from_name(name), "", name)) + else: + # Rule 3: external + dep_topic_ids.append(f"{ecosystem}/{name}") + + scope_topics.append(Topic( + id=scope_topic_ids[scope.subroot], + description=scope.description, + dependencies=dep_topic_ids, + )) # Compute common ancestor topic if >1 scope common_ancestor_topic_id = compute_common_ancestor_topic_id( diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index 7ab8a63..b0a2423 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -420,6 +420,31 @@ def test_merge_multiple_memories(self): assert len(merged.domain_constants) == 1 +class TestTopicDependencies: + """Tests for Topic.dependencies field.""" + + def test_topic_with_dependencies(self): + """Topic can have dependencies.""" + topic = Topic( + id="owner/repo/auth", + description="Auth service", + dependencies=["owner/repo/core", "PyPI/passlib"] + ) + assert topic.dependencies == ["owner/repo/core", "PyPI/passlib"] + + def test_topic_dependencies_default_empty(self): + """Topic dependencies default to empty list.""" + topic = Topic(id="owner/repo/auth", description="Auth service") + assert topic.dependencies == [] + + def test_topic_deserialization_without_dependencies(self): + """Old episodes without dependencies deserialize to empty list.""" + import json + old_data = '{"id": "owner/repo/auth", "description": "Auth service"}' + topic = Topic.model_validate_json(old_data) + assert topic.dependencies == [] + + class TestScopeResultTopicHelper: """Tests for ScopeResult.topic() helper method.""" From 9488d495adff5763c9f5c0feeb1002adc2a6291c Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Sun, 16 Aug 2026 22:54:08 +0200 Subject: [PATCH 61/79] wip --- .../agents/reviewer/modules/auditor.py | 196 ++++++++++++++---- src/codespy/agents/reviewer/reviewer.py | 23 +- 2 files changed, 168 insertions(+), 51 deletions(-) diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 96cf040..173be1e 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Sequence import dspy +import litellm from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus @@ -18,6 +19,11 @@ logger = logging.getLogger(__name__) +def _strip_patches(files: Sequence[ChangedFile]) -> list[ChangedFile]: + """Remove patch content from files to reduce token usage.""" + return [f.model_copy(update={"patch": None}) for f in files] + + class AuditSignature(dspy.Signature): """Assess code quality and provide a recommendation for a merge request. @@ -54,6 +60,109 @@ def __init__(self) -> None: self._cost_tracker = get_cost_tracker() self._settings = get_settings() + def _would_overflow_context( + self, + mr_title: str, + summary: str, + changed_files: list[ChangedFile], + all_issues: list[Issue], + context_memory: str | None = None, + ) -> bool: + """Estimate whether the input would overflow the model's context window. + + Uses litellm.token_counter for estimation with a safety margin to + account for DSPy formatting overhead (system prompt, field descriptions, + ChainOfThought instructions). + """ + SAFETY_MARGIN = 4096 # DSPy formatting overhead + token counting imprecision + + try: + llm_config = self._settings.get_llm_config("audit") + model = llm_config.model + max_tokens = llm_config.max_tokens or self._settings.default_max_tokens + + # Get model limits + info = litellm.get_model_info(model) + max_input = info.get("max_input_tokens") or 0 + max_output = info.get("max_output_tokens") or 0 + if not max_input: + return False # Unknown model, can't estimate + + # Use max_input as context window proxy (conservative). + # For shared-budget models the true window is slightly larger, + # but using max_input ensures we never overshoot. + context_window = max_input + + # Estimate input tokens from a rough serialization + input_text = f"{mr_title}\n{summary}\n{changed_files}\n{all_issues}" + if context_memory: + input_text += f"\n{context_memory}" + estimated_input = litellm.token_counter(model=model, text=input_text) + + return (estimated_input + max_tokens + SAFETY_MARGIN) > context_window + except Exception: + return False # Estimation failed; proceed with full input + + def _call_auditor( + self, + auditor: dspy.ChainOfThought, + review_context: ReviewContext, + audit_files: list[ChangedFile], + all_issues: list[Issue], + run_id: str | None, + scopes: list["ScopeResult"] | None, + topic_ids: list[str] | None, + ) -> dspy.Prediction: + """Execute the auditor predictor (with or without Hippocampus memory).""" + question = ( + f"final audit of {review_context.pr_context.repo_slug}: " + f"pull request {review_context.pr_context.mr_number} " + f"{review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + ) + + if self._settings.get_memory_enabled("audit"): + mem = Hippocampus( + auditor, + budget=self._settings.get_memory_budget("audit"), + max_reflects=self._settings.get_memory_max_reflects("audit"), + question=question, + task_name="audit", + run_id=run_id, + initial_memory=review_context.memory, + topic_ids=topic_ids, + ) + result = mem( + mr_title=review_context.pr_context.mr_title, + summary=review_context.pr_context.summary, + changed_files=audit_files, + all_issues=all_issues, + ) + mem.end_episode( + get_memory_store(self._settings), + f"/{review_context.pr_context.repo_slug}/", + artifacts={ + "audit": ( + f"## Quality Assessment\n\n{result.quality_assessment}\n\n" + f"## Recommendation\n\n{result.recommendation}\n" + ) + }, + ) + # Persist episode at each scope location if scopes are provided + if scopes: + store = get_memory_store(self._settings) + for scope in scopes: + path = mem.episode_file_path(scope.scope_path()) + mem.save_episode(store, path) + else: + result = auditor( + mr_title=review_context.pr_context.mr_title, + summary=review_context.pr_context.summary, + changed_files=audit_files, + all_issues=all_issues, + ) + + return result + def forward( self, review_context: ReviewContext, @@ -86,53 +195,54 @@ def forward( auditor = dspy.ChainOfThought(AuditSignature) logger.info("Running audit...") - question = ( - f"final audit of {review_context.pr_context.repo_slug}: " - f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + # Pre-flight: check if full input would overflow + context_memory_str = ( + review_context.memory.render() if review_context.memory else None ) + if self._would_overflow_context( + mr_title=review_context.pr_context.mr_title, + summary=review_context.pr_context.summary, + changed_files=list(changed_files), + all_issues=list(all_issues), + context_memory=context_memory_str, + ): + logger.info( + "Pre-flight: stripping patches from changed_files to fit context window" + ) + audit_files = _strip_patches(changed_files) + else: + audit_files = list(changed_files) with SignatureContext("audit", self._cost_tracker): - mem: Hippocampus | None = None - if self._settings.get_memory_enabled("audit"): - mem = Hippocampus( + try: + result = self._call_auditor( auditor, - budget=self._settings.get_memory_budget("audit"), - max_reflects=self._settings.get_memory_max_reflects("audit"), - question=question, - task_name="audit", - run_id=run_id, - initial_memory=review_context.memory, - topic_ids=topic_ids, - ) - result = mem( - mr_title=review_context.pr_context.mr_title, - summary=review_context.pr_context.summary, - changed_files=list(changed_files), - all_issues=list(all_issues), - ) - mem.end_episode( - get_memory_store(self._settings), - f"/{review_context.pr_context.repo_slug}/", - artifacts={ - "audit": ( - f"## Quality Assessment\n\n{result.quality_assessment}\n\n" - f"## Recommendation\n\n{result.recommendation}\n" - ) - }, + review_context, + audit_files, + list(all_issues), + run_id, + scopes, + topic_ids, ) - else: - result = auditor( - mr_title=review_context.pr_context.mr_title, - summary=review_context.pr_context.summary, - changed_files=list(changed_files), - all_issues=list(all_issues), - ) - - # Persist episode at each scope location if memory is enabled and scopes are provided - if mem is not None and scopes: - store = get_memory_store(self._settings) - for scope in scopes: - path = mem.episode_file_path(scope.scope_path()) - mem.save_episode(store, path) + except dspy.ContextWindowExceededError: + if audit_files is not _strip_patches(changed_files): + # Pre-flight didn't strip — try again without patches + logger.warning( + "Context window exceeded despite pre-flight check; " + "retrying without patches" + ) + audit_files = _strip_patches(changed_files) + result = self._call_auditor( + auditor, + review_context, + audit_files, + list(all_issues), + run_id, + scopes, + topic_ids, + ) + else: + # Already stripped patches and still overflowing — re-raise + raise return result.quality_assessment, result.recommendation diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index e13fd2f..6ca75f4 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -259,14 +259,21 @@ def forward(self, config: ReviewConfig) -> ReviewResult: f"Audit input: {len(scoped_files)} in-scope files " f"(filtered from {len(mr.changed_files)} total)" ) - quality_assessment, recommendation = self.auditor( - review_context=review_ctx, - changed_files=scoped_files, - all_issues=all_issues, - run_id=run_id, - scopes=scopes, - topic_ids=all_scope_topic_ids, - ) + try: + quality_assessment, recommendation = self.auditor( + review_context=review_ctx, + changed_files=scoped_files, + all_issues=all_issues, + run_id=run_id, + scopes=scopes, + topic_ids=all_scope_topic_ids, + ) + except dspy.ContextWindowExceededError: + logger.warning( + "Audit skipped: input exceeds model context window even without patches." + ) + quality_assessment = "Audit skipped due to context window limit." + recommendation = "NEEDS_DISCUSSION" if all_issues else "APPROVE" # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() return ReviewResult( From db7ae23a61327044d85b9a7faf522d73d6f60060 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 00:16:24 +0200 Subject: [PATCH 62/79] wip --- .env.example | 26 +++++++++---------- codespy.yaml | 6 ++--- .../agents/memory/hippocampus/budget.py | 6 ++--- .../agents/memory/hippocampus/hippocampus.py | 23 ++++++++-------- .../agents/reviewer/modules/auditor.py | 3 ++- .../agents/reviewer/modules/scope_resolver.py | 2 +- .../agents/reviewer/modules/summarizer.py | 3 ++- src/codespy/config_memory.py | 6 ++--- 8 files changed, 39 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index edd78a1..9a635a5 100644 --- a/.env.example +++ b/.env.example @@ -187,13 +187,13 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # 1. Ceiling on the rendered ContextMemory. This is the persisted artifact, and it is # prepended to every agent iteration, so it is re-sent ~DEFAULT_MAX_ITERS times # per scope. Divided by MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS it gives the memory's item -# capacity (3072 / 240 ~= 12 items). -# MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS=3072 +# capacity (8192 / 410 ~= 19 items). +# MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS=8192 # 2. Budget for a SINGLE context-memory item, given to the Distiller and the # Cartographer as a prompt input so no one item eats the whole memory. Soft limit # (expressed to the LLM, not enforced — truncating an item could corrupt an exact # constant). Lower it for more, terser items; raise it for fewer, richer ones. -# MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS=240 +# MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS=410 # 3. Cap on the trajectory fed to the Distiller. Tool-using agents can produce # 100k+ token trajectories and TwoStepAdapter sends the value twice, so keep this # to ~5-10% of the reflection model's context window. Unset = full trajectory @@ -272,8 +272,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 +# CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -283,8 +283,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_SCAN_UNCHANGED=false # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 +# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -292,8 +292,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 -# DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 -# DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 +# DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 # DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # DOC_MEMORY_MAX_QUESTION_TOKENS=2048 @@ -303,8 +303,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SCOPE_MAX_TOKENS=64000 # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 -# SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 -# SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 +# SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # Unused: scope uses question_field="mr_title", so no inputs are serialized. # SCOPE_MEMORY_MAX_QUESTION_TOKENS= @@ -313,8 +313,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # SUMMARY_MEMORY_ENABLED=true # SUMMARY_MEMORY_MAX_REFLECTS=1 -# SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=3072 -# SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS=240 +# SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 +# SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS=8192 # SUMMARY_MEMORY_MAX_QUESTION_TOKENS=2048 diff --git a/codespy.yaml b/codespy.yaml index 3249fbf..b54e408 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -90,7 +90,7 @@ memory: # 1. max_context_memory_tokens — ceiling on the rendered ContextMemory. This is the # persisted artifact, and it is prepended to every agent iteration, so it is # re-sent ~default_max_iters times per scope. Divided by max_context_item_tokens it - # gives the memory's item capacity (3072 / 240 ~= 12 items). + # gives the memory's item capacity (8192 / 410 ~= 19 items). # 2. max_context_item_tokens — budget for a SINGLE context memory item, given to the Distiller # and the Cartographer as a prompt input so no one item eats the whole memory. # Soft limit (expressed to the LLM, not enforced — truncating an item could @@ -103,8 +103,8 @@ memory: # 4. max_question_tokens — cap on the serialized agent inputs used as the reflection # "question". Without it, every input field is sent in full (for code review that # means the complete patch of every changed file). null = unbounded. - default_max_context_memory_tokens: 3072 # MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS - default_max_context_item_tokens: 240 # MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS + default_max_context_memory_tokens: 8192 # MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS + default_max_context_item_tokens: 410 # MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS default_max_trajectory_tokens: 8192 # MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS default_max_question_tokens: 2048 # MEMORY_DEFAULT_MAX_QUESTION_TOKENS diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index debfe9c..f6e2863 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -31,7 +31,7 @@ class MemoryBudget: wrapped agent, so it is re-sent on every agent iteration (~``max_iters`` times per run) plus once per reflection call — the most cost-sensitive of the four. Divided by ``max_context_item_tokens`` it - gives the memory's approximate item capacity (3072 / 240 ~= 12 items). + gives the memory's approximate item capacity (8192 / 410 ~= 19 items). max_context_item_tokens: Budget for a *single* context memory item, passed to the Distiller and the Cartographer as a prompt input so they keep each item compact rather than spending the whole memory budget on one @@ -59,8 +59,8 @@ class MemoryBudget: field cleanly captures intent. """ - max_context_memory_tokens: int = 3072 - max_context_item_tokens: int = 240 + max_context_memory_tokens: int = 8192 + max_context_item_tokens: int = 410 max_trajectory_tokens: int | None = 8192 max_question_tokens: int | None = 2048 diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index b912506..9ac63aa 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -325,20 +325,21 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: def episode_file_path(self, dir: str, index: int = 0) -> str: """Build the full episode file path from a directory. - Prepends the ``episodes`` root and appends a hidden ``.codespy`` - folder holding the episode file, named after the pipeline run's - identifier, the wrapped task, and an optional index to avoid collisions: - ``global/episodic//.codespy/--.json``. + Path format: ``orgs//episodic/.codespy/.[.].--.json`` Args: dir: Directory identifying where this episode belongs (e.g. a - scope's ``/{repo}/{subroot}/`` path). - index: Episode index for this scope/task combination. Used to - disambiguate when the same signature is invoked multiple - times on the same scope within a single pipeline run. + scope's ``/{host}/{owner}/{repo}/{subroot}/`` path or + ``/{owner}/{repo}/{subroot}/`` without host). + index: Episode index for this scope/task combination. """ - trimmed = dir.strip("/") - return f"global/episodic/{trimmed}/.codespy/{self._run_id}-{self._task_name}-{index}.json" + segments = [s for s in dir.strip("/").split("/") if s] + # Strip host segment (contains a dot, e.g. github.com/gitlab.com) + if segments and "." in segments[0]: + segments = segments[1:] + owner = segments[0] if segments else "unknown" + slug = ".".join(segments) + return f"orgs/{owner}/episodic/.codespy/{slug}.{self._run_id}-{self._task_name}-{index}.json" def end_episode( self, @@ -360,7 +361,7 @@ def end_episode( If both ``store`` and ``dir`` are provided the episode is persisted via ``save_episode()`` after consolidation, at - ``global/episodic//.codespy/-.json``. ``store`` may be a + ``orgs//episodic/.codespy/.--.json``. ``store`` may be a ``FileSystem`` or an ``S3Client`` instance. Args: diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 173be1e..5f4f637 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -9,6 +9,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, ReviewContext +from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.models import ChangedFile @@ -139,7 +140,7 @@ def _call_auditor( ) mem.end_episode( get_memory_store(self._settings), - f"/{review_context.pr_context.repo_slug}/", + _deepest_common_folder(scopes, review_context.pr_context.repo_slug) if scopes else f"/{review_context.pr_context.repo_slug}/", artifacts={ "audit": ( f"## Quality Assessment\n\n{result.quality_assessment}\n\n" diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index f899ac5..e64d074 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -313,7 +313,7 @@ class ScopeRefinementSignature(dspy.Signature): OUTPUT: Final refined scope boundaries. Files are assigned automatically. - For each scope boundary, include a `description` (max 500 characters) summarizing + For each scope boundary, include a `description` (max 615 tokens) summarizing what the folder contains and its role in the project. For example: - "Auth library handling JWT issuance and session management" - "API gateway routing to downstream services" diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 92fa233..a82c467 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -7,6 +7,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus +from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder from codespy.config import get_settings from codespy.config_memory import get_memory_store @@ -107,7 +108,7 @@ def forward( ) mem.end_episode( get_memory_store(self._settings), - f"/{repo_slug}/", + _deepest_common_folder(scopes, repo_slug) if scopes else f"/{repo_slug}/", artifacts={"summary": result.summary}, ) else: diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index eb5f7cf..8a7b4fc 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -82,9 +82,9 @@ class MemoryConfig(BaseModel): # every ReAct iteration (~default_max_iters times per scope) plus once per # reflection call. Easily the most cost-sensitive of the three budgets. # Approximate item capacity is default_max_context_memory_tokens divided by - # default_max_context_item_tokens (3072 / 240 ~= 12 items). + # default_max_context_item_tokens (8192 / 410 ~= 19 items). # MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS - default_max_context_memory_tokens: int = Field(default=3072) + default_max_context_memory_tokens: int = Field(default=8192) # Per-item ceiling handed to the Distiller/Cartographer as a prompt input, so # they keep each context-memory item compact instead of spending the whole memory @@ -92,7 +92,7 @@ class MemoryConfig(BaseModel): # than enforced in code (truncating an item could corrupt an exact constant). # The hard, memory-wide limit is default_max_context_memory_tokens, enforced by the # Evictor. MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS - default_max_context_item_tokens: int = Field(default=240) + default_max_context_item_tokens: int = Field(default=410) # Head+tail cap on the agent trajectory fed to the Distiller. Without it a From d01c8022f6b409d4b37b111a2ad9c8fe03a69d82 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 00:34:00 +0200 Subject: [PATCH 63/79] wip --- .../agents/memory/hippocampus/__init__.py | 3 +- .../agents/memory/hippocampus/episode.py | 74 ++++++++++++++++++- .../agents/reviewer/modules/scope_resolver.py | 47 ++++++++++-- .../agents/reviewer/modules/summarizer.py | 17 ++++- .../tools/storage/filesystem/client.py | 7 +- src/codespy/tools/storage/models.py | 1 + src/codespy/tools/storage/s3/client.py | 7 +- 7 files changed, 145 insertions(+), 11 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index a56e4f6..3da84ee 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -12,7 +12,7 @@ compute_common_ancestor_topic_id, make_topic_id, ) -from codespy.agents.memory.hippocampus.episode import Episode +from codespy.agents.memory.hippocampus.episode import Episode, find_latest_episode from codespy.agents.memory.hippocampus.hippocampus import Hippocampus from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer, CartographerSig from codespy.agents.memory.hippocampus.modules.distiller import Distiller, DistillerSig @@ -25,6 +25,7 @@ "Distiller", "DistillerSig", "Episode", + "find_latest_episode", "Hippocampus", "Item", "ItemTag", diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 7fa52c6..09ceb4c 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -2,12 +2,13 @@ from __future__ import annotations -from datetime import UTC, datetime +from datetime import UTC, datetime, timezone from pydantic import BaseModel, Field from codespy.agents.memory.hippocampus.context_memory import ContextMemory, Mutation from codespy.tools.storage.base import Storage +from codespy.tools.storage.models import Entry, EntryType class Episode(BaseModel): @@ -111,3 +112,74 @@ def load_episode(store: Storage, path: str) -> Episode: except Exception as exc: raise OSError(f"Failed to parse episode from {path!r}: {exc}") from exc return episode + + +def find_latest_episode( + store: Storage, + dir: str, + task: str | None = None, + exclude_run_id: str | None = None, +) -> Episode | None: + """Find and load the most recent episode for a given scope path. + + Searches ``orgs/{owner}/episodic/.codespy/`` for episodes whose filename + starts with the slug derived from ``dir`` (same logic as + ``Hippocampus.episode_file_path``). Optionally filters by task name and + excludes a specific run_id. + + Args: + store: Storage backend (FileSystem or S3Client). + dir: Scope directory path (e.g., "/{repo_slug}/{subroot}/"). + Host segments (containing a dot) are stripped automatically. + task: Optional task filter (e.g., "scope", "summary"). + Matches ``-{task}-`` substring in filename remainder. + If None, any task matches. + exclude_run_id: If set, skip episodes containing this run_id in + filename (avoids loading current pipeline's own episodes). + + Returns: + The most recent Episode by modified_at, or None if no matches found. + """ + # Compute slug and episodic directory (mirrors Hippocampus.episode_file_path) + segments = [s for s in dir.strip("/").split("/") if s] + if segments and "." in segments[0]: + segments = segments[1:] + if not segments: + return None + owner = segments[0] + slug = ".".join(segments) + episodic_dir = f"orgs/{owner}/episodic/.codespy" + + try: + listing = store.list_directory(episodic_dir) + except (FileNotFoundError, OSError): + return None + # Filter entries: prefix match + optional task + exclude run_id + # Filename: {slug}.{run_id}-{task}-{index}.json + prefix = f"{slug}." + candidates: list[Entry] = [] + for entry in listing.entries: + if entry.entry_type != EntryType.FILE: + continue + if not entry.name.startswith(prefix): + continue + remainder = entry.name[len(prefix):] + if task is not None and f"-{task}-" not in remainder: + continue + if exclude_run_id and exclude_run_id in remainder: + continue + candidates.append(entry) + if not candidates: + return None + # Sort by modified_at descending; epoch fallback for entries without timestamp + _epoch = datetime.min.replace(tzinfo=timezone.utc) + candidates.sort( + key=lambda e: e.modified_at if e.modified_at is not None else _epoch, + reverse=True, + ) + # Load the newest candidate + path = f"{episodic_dir}/{candidates[0].name}" + try: + return load_episode(store, path) + except (FileNotFoundError, OSError): + return None diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index e64d074..21c4f1e 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -21,6 +21,7 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import ( + PRContext, PackageManifest, ReviewContext, ScopeResult, @@ -1115,12 +1116,9 @@ async def aforward( """ excluded_dirs = self._settings.excluded_directories reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] - if not reviewable_files: return [], review_context.memory if review_context else None - repo = mr.repo_slug - if not self._settings.is_signature_enabled("scope"): fallback = ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, @@ -1135,7 +1133,6 @@ async def aforward( try: await self._ensure_repo(mr, repo_path, is_local) scopes, orphans = self._resolve(repo_path, reviewable_files, repo) - # Log deterministic scopes before LLM refinement if scopes: det_summary = "\n".join( @@ -1150,7 +1147,47 @@ async def aforward( ) if orphans: logger.info("Deterministic identification produced %d orphan(s)", len(orphans)) - + # Load prior scope memory from the deepest common folder + loaded_memory: ContextMemory | None = None + if self._settings.get_memory_enabled("scope"): + from codespy.agents.memory.hippocampus.episode import find_latest_episode + store = get_memory_store(self._settings) + common_dir = _deepest_common_folder(scopes, mr.repo_slug) if scopes else f"/{mr.repo_slug}/" + prior_episode = find_latest_episode(store, common_dir, task="scope", exclude_run_id=run_id) + # Fallback: prior run may have persisted at repo root if scopes differed + if prior_episode is None and common_dir != f"/{mr.repo_slug}/": + prior_episode = find_latest_episode( + store, f"/{mr.repo_slug}/", task="scope", exclude_run_id=run_id + ) + if prior_episode is not None: + loaded_memory = prior_episode.context_memory + # Strip prior-run topics: current run builds authoritative topics + # via bind_topics after refinement. Clearing item topic_ids ensures + # bind_topics can re-bind them to the current run's stamp_topic_ids. + loaded_memory.topics = [] + for item in loaded_memory.all_items(): + item.topic_ids = [] + logger.info( + "Loaded prior scope memory (run=%s, items=%d)", + prior_episode.run_id[:8], len(loaded_memory.all_items()), + ) + else: + logger.debug("No prior scope episode found at %s", common_dir) + # Inject loaded memory into review_context for _refine_scopes + if loaded_memory is not None: + review_context = ReviewContext( + pr_context=review_context.pr_context if review_context else PRContext( + repo_slug=mr.repo_slug, + mr_number=mr.number, + mr_title=mr.title or "", + summary=mr.title or "", + ), + memory=( + ContextMemory.merge(loaded_memory, review_context.memory) + if review_context and review_context.memory + else loaded_memory + ), + ) scopes, context_memory = await self._refine_scopes( scopes, orphans, mr, repo_path, review_context, run_id ) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index a82c467..27da842 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -81,7 +81,22 @@ def forward( if not self._settings.is_signature_enabled("summary"): logger.debug("Skipping summary: disabled") return mr_title or "No title", initial_memory - + # Load latest episode per scope and merge with inherited memory + if self._settings.get_memory_enabled("summary") and scopes: + from codespy.agents.memory.hippocampus.episode import find_latest_episode + store = get_memory_store(self._settings) + per_scope_memories: list[ContextMemory] = [] + for scope in scopes: + ep = find_latest_episode(store, scope.scope_path(), task=None, exclude_run_id=run_id) + if ep is not None: + per_scope_memories.append(ep.context_memory) + if per_scope_memories: + all_memories = ([initial_memory] if initial_memory else []) + per_scope_memories + initial_memory = ContextMemory.merge(*all_memories) + logger.info( + "Merged %d prior scope episode(s) into summarizer memory", + len(per_scope_memories), + ) summarizer = dspy.ChainOfThought(PRSummarySignature) logger.info("Generating PR summary...") diff --git a/src/codespy/tools/storage/filesystem/client.py b/src/codespy/tools/storage/filesystem/client.py index 4e99cfc..df0380b 100644 --- a/src/codespy/tools/storage/filesystem/client.py +++ b/src/codespy/tools/storage/filesystem/client.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from datetime import datetime, timezone from pathlib import Path from codespy.tools.storage.base import Storage @@ -132,9 +133,11 @@ def list_directory( entry_type = EntryType.FILE total_files += 1 - size = entry.stat().st_size if entry_type == EntryType.FILE else 0 + stat = entry.stat() + size = stat.st_size if entry_type == EntryType.FILE else 0 + modified_at = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) if entry_type == EntryType.FILE else None - entries.append(Entry(name=entry.name, entry_type=entry_type, size=size)) + entries.append(Entry(name=entry.name, entry_type=entry_type, size=size, modified_at=modified_at)) except PermissionError as e: logger.warning(f"Permission denied listing {path}: {e}") diff --git a/src/codespy/tools/storage/models.py b/src/codespy/tools/storage/models.py index 1ca9d7b..7704e8b 100644 --- a/src/codespy/tools/storage/models.py +++ b/src/codespy/tools/storage/models.py @@ -67,6 +67,7 @@ class Entry(BaseModel): name: str = Field(description="Entry name") entry_type: EntryType = Field(description="Type of entry") size: int = Field(default=0, description="Size in bytes (0 for directories)") + modified_at: datetime | None = Field(default=None, description="Last modified time (UTC)") class Listing(BaseModel): diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index 4752d96..e2eea38 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -206,7 +206,12 @@ def list_directory( if not include_hidden and name.startswith("."): continue entries.append( - Entry(name=name, entry_type=EntryType.FILE, size=obj.get("Size", 0)) + Entry( + name=name, + entry_type=EntryType.FILE, + size=obj.get("Size", 0), + modified_at=obj.get("LastModified"), # boto3 returns tz-aware datetime + ) ) total_files += 1 From 6620cdbd1856e009998f22dc256ecc324585eed0 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 01:01:48 +0200 Subject: [PATCH 64/79] wip --- README.md | 649 +++--------------------------------------- codespy.yaml | 15 + docs/architecture.md | 135 +++++++++ docs/configuration.md | 199 +++++++++++++ docs/development.md | 63 ++++ docs/memory.md | 117 ++++++++ docs/usage.md | 281 ++++++++++++++++++ 7 files changed, 843 insertions(+), 616 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/configuration.md create mode 100644 docs/development.md create mode 100644 docs/memory.md create mode 100644 docs/usage.md diff --git a/README.md b/README.md index 54718db..31280ac 100644 --- a/README.md +++ b/README.md @@ -36,26 +36,7 @@ - [Using Docker](#using-docker) - [Using Poetry (for development)](#using-poetry-for-development) - [Quick Start](#quick-start) -- [Usage](#usage) - - [Command Line](#command-line) - - [IDE Integration (MCP Server)](#ide-integration-mcp-server) - - [Using Docker](#using-docker-1) - - [GitHub Action](#github-action) -- [Configuration](#configuration) - - [Setup](#setup) - - [Git Platform Tokens](#git-platform-tokens) - - [GitHub Token](#github-token) - - [GitLab Token](#gitlab-token) - - [LLM Provider](#llm-provider) - - [Advanced Configuration (YAML)](#advanced-configuration-yaml) - - [Recommended Model Strategy](#recommended-model-strategy) -- [Output](#output) - - [Markdown (default)](#markdown-default) - - [GitHub/GitLab Review Comments](#githubgitlab-review-comments) -- [Architecture](#architecture) -- [DSPy Signatures](#dspy-signatures) -- [Supported Languages](#supported-languages) -- [Development](#development) +- [Documentation](#documentation) - [Contributors](#contributors) - [License](#license) @@ -64,40 +45,37 @@ ## Why CodeSpy? Most AI code reviewers are: -- ❌ Black boxes -- ❌ SaaS-only -- ❌ Opaque about reasoning -- ❌ Risky for sensitive codebases +- ❌ Black boxes +- ❌ SaaS-only +- ❌ Opaque about reasoning +- ❌ Risky for sensitive codebases **CodeSpy is different:** -- 🔍 Transparent reasoning -- 🔐 Self-hostable -- 🧠 Configurable review rules -- 🔄 Native PR integration -- 🧩 Extensible architecture -- 📦 100% open-source +- 🔍 Transparent reasoning +- 🔐 Self-hostable +- 🔄 Native PR integration +- 🧩 Extensible architecture +- 📦 100% open-source Built for **engineering teams that care about correctness, security, and control.** --- - ## Features -- 🔒 **Security Analysis** - Detects common vulnerabilities (injection, auth issues, data exposure, etc.) with CWE references -- 🐛 **Bug Detection** - Identifies logic errors, null references, resource leaks, edge cases -- 📝 **Documentation Review** - Checks for missing docstrings, outdated comments, incomplete docs -- 🔍 **Intelligent Scope Detection** - Automatically identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) -- 💰 **Cost Tracking** - Track LLM calls, tokens, and costs per review -- 🤖 **Model Agnostic** - Works with OpenAI, AWS Bedrock, Anthropic, Ollama, and more via LiteLLM -- 🐳 **Docker Ready** - Run locally or in the cloud with Docker -- GitHub GitLab **GitHub & GitLab** - Works with both platforms, auto-detects from URL -- 🖥️ **Local Reviews** - Review local git changes without GitHub/GitLab — diff against any branch, ref, or review uncommitted work -- 🧩 **MCP Server** - IDE integration via Model Context Protocol — trigger reviews from AI coding assistants like Cline without leaving your editor -- 🔌 **GitHub Action** - One-line integration for automatic PR reviews - - +- 🔒 **Security Analysis** — Detects common vulnerabilities (injection, auth issues, data exposure) with CWE references +- 🐛 **Bug Detection** — Identifies logic errors, null references, resource leaks, edge cases +- 📝 **Documentation Review** — Checks for missing docstrings, outdated comments, incomplete docs +- 🔍 **Intelligent Scope Detection** — Automatically identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) +- 🧠 **Cross-Review Memory** — Agents learn patterns, constants, and domain knowledge from past reviews of the same codebase +- 💰 **Cost Tracking** — Track LLM calls, tokens, and costs per review +- 🤖 **Model Agnostic** — Works with OpenAI, AWS Bedrock, Anthropic, Gemini, Ollama, and more via LiteLLM +- 🐳 **Docker Ready** — Run locally or in the cloud with Docker +- GitHub GitLab **GitHub & GitLab** — Works with both platforms, auto-detects from URL +- 🖥️ **Local Reviews** — Review local git changes without GitHub/GitLab — diff against any branch, ref, or review uncommitted work +- 🧩 **MCP Server** — IDE integration via Model Context Protocol — trigger reviews from AI coding assistants without leaving your editor +- 🔌 **GitHub Action** — One-line integration for automatic PR reviews --- @@ -162,585 +140,24 @@ codespy review https://github.com/owner/repo/pull/123 codespy review https://gitlab.com/group/project/-/merge_requests/123 ``` -codespy auto-discovers credentials from standard locations (`~/.aws/credentials`, `gh auth token`, `glab auth token`, etc.) - see [Configuration](#configuration) for details. - ---- - -## Usage - -### Command Line - -```bash -# Review GitHub Pull Request -codespy review https://github.com/owner/repo/pull/123 - -# Review GitLab Merge Request -codespy review https://gitlab.com/group/project/-/merge_requests/123 - -# GitLab with nested groups -codespy review https://gitlab.com/group/subgroup/project/-/merge_requests/123 - -# Self-hosted GitLab -codespy review https://gitlab.mycompany.com/team/project/-/merge_requests/123 - -# Output as JSON -codespy review https://github.com/owner/repo/pull/123 --output json - -# Use a specific model -codespy review https://github.com/owner/repo/pull/123 --model anthropic/claude-opus-4-6 - -# Use a custom config file -codespy review https://github.com/owner/repo/pull/123 --config path/to/config.yaml -codespy review https://github.com/owner/repo/pull/123 -f staging.yaml - -# Disable stdout output (useful with --git-comment) -codespy review https://github.com/owner/repo/pull/123 --no-stdout - -# Post review as GitHub/GitLab comment -codespy review https://github.com/owner/repo/pull/123 --git-comment - -# Combine: only post to Git platform, no stdout -codespy review https://github.com/owner/repo/pull/123 --no-stdout --git-comment - -# Show current configuration -codespy config - -# Show configuration from a specific file -codespy config --config path/to/config.yaml - -# Show version -codespy --version - -# Review local git changes (no GitHub/GitLab needed) -codespy review-local # Review current dir vs main -codespy review-local /path/to/repo # Review specific repo -codespy review-local --base develop # Compare against develop -codespy review-local --base origin/main # Compare against origin/main -codespy review-local --base HEAD~5 # Compare against 5 commits back - -# Review uncommitted changes (staged + unstaged) -codespy review-uncommitted # Review current dir -codespy review-uncommitted /path/to/repo -codespy review-uncommitted --output json -``` - -### IDE Integration (MCP Server) - -CodeSpy can run as an MCP (Model Context Protocol) server for integration with AI coding assistants like Cline, enabling code reviews directly from your editor without leaving your workflow. - -```bash -# Start the MCP server -codespy serve - -# Use a custom config file -codespy serve --config path/to/config.yaml -``` - -**Configure your IDE** (example for Cline in VS Code): - -Add to `cline_mcp_settings.json`: -```json -{ - "mcpServers": { - "codespy-reviewer": { - "command": "codespy", - "args": ["serve"], - "env": { - "DEFAULT_MODEL": "anthropic/claude-opus-4-6", - "ANTHROPIC_API_KEY": "your-key-here" - } - } - } -} -``` - -Or for AWS Bedrock: -```json -{ - "mcpServers": { - "codespy-reviewer": { - "command": "codespy", - "args": ["serve"], - "env": { - "DEFAULT_MODEL": "bedrock/us.anthropic.claude-opus-4-6-v1", - "AWS_REGION": "us-east-1", - "AWS_ACCESS_KEY_ID": "your-access-key", - "AWS_SECRET_ACCESS_KEY": "your-secret-key" - } - } - } -} -``` - -**Available MCP Tools:** -- `review_local_changes(repo_path, base_ref)` — Review branch changes vs base (e.g., vs `main`) -- `review_uncommitted(repo_path)` — Review staged + unstaged working tree changes -- `review_pr(mr_url)` — Review a GitHub PR or GitLab MR by URL - -Then ask your AI assistant: *"Review my local changes"* or *"Review uncommitted work in /path/to/repo"* - -### Using Docker - -```bash -# With docker run (using GHCR image) -docker run --rm \ - -e GITHUB_TOKEN=$GITHUB_TOKEN \ - -e DEFAULT_MODEL=anthropic/claude-opus-4-6 \ - -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ - ghcr.io/khezen/codespy:latest review https://github.com/owner/repo/pull/123 - -# Or use a specific version -docker run --rm \ - -e GITHUB_TOKEN=$GITHUB_TOKEN \ - -e DEFAULT_MODEL=anthropic/claude-opus-4-6 \ - -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ - ghcr.io/khezen/codespy:0.2.1 review https://github.com/owner/repo/pull/123 -``` - -### GitHub Action - -Add CodeSpy to your repository for automatic PR reviews: - -**Trigger on `/codespy review` comment:** - -```yaml -# .github/workflows/codespy-review.yml -name: CodeSpy Code Review - -on: - issue_comment: - types: [created] - -jobs: - review: - # Only run on PR comments containing '/codespy review' - if: | - github.event.issue.pull_request && - contains(github.event.comment.body, '/codespy review') - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - name: Run CodeSpy Review - uses: khezen/codespy@v1 - with: - model: 'anthropic/claude-opus-4-6' - anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} -``` - -**Trigger automatically on every PR:** - -```yaml -# .github/workflows/codespy-review.yml -name: CodeSpy Code Review - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - review: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - name: Run CodeSpy Review - uses: khezen/codespy@v1 - with: - model: 'anthropic/claude-opus-4-6' - anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} -``` - -See [`.github/workflows/codespy-review.yml.example`](.github/workflows/codespy-review.yml.example) for more examples. - ---- - -## Configuration - -codespy supports two configuration methods: -- **`.env` file** - Simple environment variables for basic setup -- **`codespy.yaml`** - Full YAML configuration for advanced options (per-module settings) - -Priority: cmd options > Environment Variables > YAML Config > Defaults - -### Setup - -```bash -# Copy the example file -cp .env.example .env -``` - -### Git Platform Tokens - -codespy automatically detects the platform (GitHub or GitLab) from the URL and discovers tokens from multiple sources. - -#### GitHub Token - -Auto-discovered from: -- `GITHUB_TOKEN` or `GH_TOKEN` environment variables -- GitHub CLI (`gh auth token`) -- Git credential helper -- `~/.netrc` file - -Or create a token at https://github.com/settings/tokens with `repo` scope: -```bash -GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx -``` - -To disable auto-discovery: -```bash -GITHUB_AUTO_DISCOVER_TOKEN=false -``` - -#### GitLab Token - -Auto-discovered from: -- `GITLAB_TOKEN` or `GITLAB_PRIVATE_TOKEN` environment variables -- GitLab CLI (`glab auth token`) -- Git credential helper -- `~/.netrc` file -- python-gitlab config files (`~/.python-gitlab.cfg`, `/etc/python-gitlab.cfg`) - -Or create a token at https://gitlab.com/-/user_settings/personal_access_tokens with `api` scope: -```bash -GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -``` - -For self-hosted GitLab: -```bash -GITLAB_URL=https://gitlab.mycompany.com -GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx -``` - -To disable auto-discovery: -```bash -GITLAB_AUTO_DISCOVER_TOKEN=false -``` - -### LLM Provider - -codespy auto-discovers credentials for all providers: - -**Anthropic** (auto-discovers from `$ANTHROPIC_API_KEY`, `~/.config/anthropic/`, `~/.anthropic/`): -```bash -DEFAULT_MODEL=anthropic/claude-opus-4-6 -# Optional - set explicitly or let codespy auto-discover: -# ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx -``` - -**AWS Bedrock** (auto-discovers from `~/.aws/credentials`, AWS CLI, env vars): -```bash -DEFAULT_MODEL=bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0 -AWS_REGION=us-east-1 -# Optional - uses ~/.aws/credentials by default, or set explicitly: -# AWS_ACCESS_KEY_ID=... -# AWS_SECRET_ACCESS_KEY=... -``` - -**OpenAI** (auto-discovers from `$OPENAI_API_KEY`, `~/.config/openai/`, `~/.openai/`): -```bash -DEFAULT_MODEL=openai/gpt-5 -# Optional - set explicitly or let codespy auto-discover: -# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx -``` - -**Google Gemini** (auto-discovers from `$GEMINI_API_KEY`, `$GOOGLE_API_KEY`, gcloud ADC): -```bash -DEFAULT_MODEL=gemini/gemini-2.5-pro -# Optional - set explicitly or let codespy auto-discover: -# GEMINI_API_KEY=xxxxxxxxxxxxxxxxxxxx -``` - -**Local Ollama:** -```bash -DEFAULT_MODEL=ollama/llama3 -``` - -To disable auto-discovery for specific providers: -```bash -AUTO_DISCOVER_AWS=false -AUTO_DISCOVER_OPENAI=false -AUTO_DISCOVER_ANTHROPIC=false -AUTO_DISCOVER_GEMINI=false -``` - -### Advanced Configuration (YAML) - -For per-signature settings, use `codespy.yaml`. See [`codespy.yaml`](codespy.yaml) for all available options including: -- LLM provider settings and auto-discovery -- Git platform configuration (GitHub/GitLab) -- Per-signature model and iteration overrides -- Output format and destination settings -- Directory exclusions - -Override YAML settings via environment variables using `_` separator: - -```bash -# Default settings -export DEFAULT_MODEL=anthropic/claude-opus-4-6 -export DEFAULT_MAX_ITERS=20 - -# Per-signature settings (use signature name, not module name) -export CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 - -# Output settings -export OUTPUT_STDOUT=false -export OUTPUT_GIT=true -``` - -See `codespy.yaml` for full configuration options. - -### Recommended Model Strategy - -codespy uses a tiered model approach to balance review quality and cost: - -| Tier | Role | Default | Recommended Model | Used By | -|------|------|---------|-------------------|---------| -| 🧠 **Smart** | Core analysis & reasoning | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Code & doc review, supply chain, scope identification | -| ⚡ **Mid-tier** | Field extraction | Falls back to `DEFAULT_MODEL` | `anthropic/claude-sonnet-4-5-20250929` | TwoStepAdapter field extraction | -| 💰 **Cheap** | Summarization | Falls back to `DEFAULT_MODEL` | `anthropic/claude-haiku-4-5-20251001` | PR summary generation | - -By default, **all models use `DEFAULT_MODEL`** (`anthropic/claude-opus-4-6`). This works out of the box — just set your API credentials and go. - -To optimize costs, override the mid-tier and cheap models: - -```bash -# .env or environment variables -DEFAULT_MODEL=anthropic/claude-opus-4-6 # Smart tier (default) -EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 # Mid-tier: field extraction -SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # Cheap tier: PR summary -``` - -Or in `codespy.yaml`: - -```yaml -default_model: anthropic/claude-opus-4-6 -extraction_model: anthropic/claude-sonnet-4-5-20250929 -signatures: - summary: - model: anthropic/claude-haiku-4-5-20251001 -``` +codespy auto-discovers credentials from standard locations (`~/.aws/credentials`, `gh auth token`, `glab auth token`, etc.) - see [Configuration](docs/configuration.md) for details. --- -## Output - -### Markdown (default) - -```markdown -# Code Review: Add user authentication - -**PR:** [owner/repo#123](https://github.com/owner/repo/pull/123) -**Reviewed at:** 2024-01-15 10:30 UTC -**Model:** anthropic/claude-opus-4-6 - -## Summary - -This PR implements user authentication with JWT tokens... - -## Statistics +## Documentation -- **Total Issues:** 3 -- **Critical:** 1 -- **Security:** 1 -- **Bugs:** 1 -- **Documentation:** 1 - -## Issues - -### 🔴 Critical (1) - -#### SQL Injection Vulnerability - -**Location:** `src/auth/login.py:45` -**Category:** security - -The user input is directly interpolated into the SQL query... - -**Code:** -query = f"SELECT * FROM users WHERE username = '{username}'" - -**Suggestion:** -Use parameterized queries instead... - -**Reference:** [CWE-89](https://cwe.mitre.org/data/definitions/89.html) - -``` - -### GitHub/GitLab Review Comments - -CodeSpy can post reviews directly to GitHub PRs or GitLab MRs as native review comments with inline annotations. - -**Enable via CLI:** -```bash -# GitHub -codespy review https://github.com/owner/repo/pull/123 --git-comment - -# GitLab -codespy review https://gitlab.com/group/project/-/merge_requests/123 --git-comment - -# Combine: only post to platform, no stdout -codespy review https://github.com/owner/repo/pull/123 --no-stdout --git-comment -``` - -**Enable via configuration:** -```bash -# Environment variable -export OUTPUT_GIT=true - -# Or in codespy.yaml -output_git: true -``` - -**Features:** - -- 🎯 **Inline Comments** - Issues are posted as review comments on the exact lines where they occur -- 📏 **Multi-line Support** - Issues spanning multiple lines are annotated with start/end line ranges -- 🔴🟠🟡🔵 **Severity Indicators** - Visual emoji markers for Critical, High, Medium, Low severity -- 📦 **Collapsible Sections** - Organized review body with expandable details: - - 📋 Summary of changes - - 🎯 Quality Assessment - - 📊 Statistics table - - 💰 Cost breakdown per signature - - 💡 Recommendation -- 🔗 **CWE References** - Security issues link directly to MITRE CWE database - ---- - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ codespy CLI │ -├─────────────────────────────────────────────────────────────────────┤ -│ review [--config ...] [--output json|md] [--model ...] │ -└──────────────────────────────┬──────────────────────────────────────┘ - │ -┌──────────────────────────────▼──────────────────────────────────────┐ -│ Git Platform Integration │ -│ - GitHub: Fetch PR diff, changed files, commit messages │ -│ - GitLab: Fetch MR diff, changed files, commit messages │ -│ - Auto-detects platform from URL │ -│ - Clone/access full repository for context │ -└──────────────────────────────┬──────────────────────────────────────┘ - │ -┌──────────────────────────────▼──────────────────────────────────────┐ -│ DSPy Review Pipeline │ -│ │ -│ ┌────────────────────────────────────────────────────────────┐ │ -│ │ Scope Identifier │ │ -│ │ (identifies code scopes: frontend, backend, infra, etc.) │ │ -│ └──────────────────────────┬─────────────────────────────────┘ │ -│ │ │ -│ ┌──────────────────────────▼─────────────────────────────────┐ │ -│ │ Parallel Review Modules │ │ -│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │ -│ │ │ Supply Chain │ │ Code │ │ Doc │ │ │ -│ │ │ Auditor │ │ Reviewer │ │ Reviewer │ │ │ -│ │ │ │ │ (bug+sec+ │ │ │ │ │ -│ │ │ │ │ smell) │ │ │ │ │ -│ │ └──────────────┘ └──────────────┘ └──────────┘ │ │ -│ └──────────────────────────┬─────────────────────────────────┘ │ -│ │ │ -│ ┌──────────────────────────▼─────────────────────────────────┐ │ -│ │ PR Summarizer │ │ -│ │ (generates summary, quality assessment, recommendation) │ │ -│ └────────────────────────────────────────────────────────────┘ │ -│ │ -│ Cost Tracker (tokens, calls, $) │ -└──────────────────────────────┬──────────────────────────────────────┘ - │ -┌──────────────────────────────▼──────────────────────────────────────┐ -│ Tools Layer │ -│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ -│ │ Filesystem │ │ Git │ │ Web │ │ Cyber/OSV │ │ -│ │ │ │ (GH + GL) │ │ │ │ │ │ -│ └────────────┘ └────────────┘ └────────────┘ └──────────────┘ │ -│ ┌────────────────────────────────────────────────────────────────┐ │ -│ │ Parsers │ │ -│ │ ┌─────────────────┐ ┌────────────────────────────────────┐ │ │ -│ │ │ Ripgrep │ │ Tree-sitter │ │ │ -│ │ │ (code search) │ │ (multi-language AST parsing) │ │ │ -│ │ └─────────────────┘ └────────────────────────────────────┘ │ │ -│ └────────────────────────────────────────────────────────────────┘ │ -└──────────────────────────────┬──────────────────────────────────────┘ - │ -┌──────────────────────────────▼──────────────────────────────────────┐ -│ LLM Backend (LiteLLM) │ -│ Bedrock | OpenAI | Anthropic | Ollama | Any OpenAI-compatible │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## DSPy Signatures - -The review is powered by DSPy signatures that structure the LLM's analysis: - -| Signature | Config Key | Description | -|-----------|------------|-------------| -| **ScopeIdentifierSignature** | `scope` | Identifies code scopes (frontend, backend, infra, microservice in mono repo, etc...) | -| **CodeReviewSignature** | `code_review` | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells | -| **DocReviewSignature** | `doc` | Detects stale or wrong documentation caused by code changes | -| **SupplyChainSecuritySignature** | `supply_chain` | Analyzes artifacts (Dockerfiles) and dependencies for supply chain security | -| **MRSummarySignature** | `summary` | Generates summary, quality assessment, and recommendation | - -## Supported Languages - -Tree-sitter based parsing for context-aware analysis: - -| Language | Extensions | Features | -|----------|-----------|----------| -| Bash | `.sh`, `.bash` | Functions, commands | -| C/C++ | `.c`, `.cpp`, `.h`, `.hpp` | Functions, classes, structs | -| C# | `.cs` | Methods, classes, interfaces | -| Go | `.go` | Functions, structs, interfaces | -| Java | `.java` | Methods, classes, packages | -| JavaScript | `.js`, `.jsx` | Functions, classes, imports | -| Kotlin | `.kt` | Functions, classes, objects | -| Objective-C | `.m`, `.h` | Methods, interfaces, protocols | -| PHP | `.php` | Functions, classes, namespaces | -| Python | `.py` | Functions, classes, imports | -| Ruby | `.rb` | Methods, classes, modules | -| Rust | `.rs` | Functions, structs, traits, impl blocks | -| Swift | `.swift` | Functions, classes, structs | -| Terraform | `.tf` | Resources, data sources, modules, variables | -| TypeScript | `.ts`, `.tsx` | Functions, classes, interfaces | - -All languages are supported for security, bug, and documentation analysis. - -## Development - -```bash -# Quick setup (creates .env and installs dependencies) -make setup - -# Or manually with Poetry: -poetry install # Install all dependencies including dev -poetry lock # Update lock file - -# Available make targets -make help - -# Run commands with Poetry -make lint # Run ruff linter -make format # Format code with ruff -make typecheck # Run mypy type checker -make test # Run pytest tests -make build # Build package with Poetry -make clean # Clean build artifacts - -# Or run directly: -poetry run codespy review https://github.com/owner/repo/pull/123 -poetry run ruff check src/ -poetry run mypy src/ -``` +| Guide | Contents | +|-------|----------| +| **[Usage](docs/usage.md)** | CLI commands, Docker, GitHub Action, MCP server, output formats | +| **[Configuration](docs/configuration.md)** | Environment variables, YAML config, model strategy, per-signature settings | +| **[Architecture](docs/architecture.md)** | Pipeline design, DSPy signatures, supported languages | +| **[Memory System](docs/memory.md)** | Hippocampus episodic memory for cross-review knowledge | +| **[Development](docs/development.md)** | Setup, build, test, lint | --- ## Contributors + * @khezen * @pranavsriram8 @@ -748,4 +165,4 @@ poetry run mypy src/ ## License -MIT \ No newline at end of file +MIT diff --git a/codespy.yaml b/codespy.yaml index b54e408..51bb75a 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -275,6 +275,21 @@ signatures: max_trajectory_tokens: null # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: null # SUMMARY_MEMORY_MAX_QUESTION_TOKENS + # Auditor signature (quality assessment + recommendation after all reviews) + audit: + enabled: true # AUDIT_ENABLED + model: null # AUDIT_MODEL + reasoning_effort: null # AUDIT_REASONING_EFFORT + temperature: null # AUDIT_TEMPERATURE + max_tokens: null # AUDIT_MAX_TOKENS + memory: + enabled: null # AUDIT_MEMORY_ENABLED (null -> memory.default_enabled) + max_reflects: null # AUDIT_MEMORY_MAX_REFLECTS + max_context_memory_tokens: null # AUDIT_MEMORY_MAX_CONTEXT_MEMORY_TOKENS + max_context_item_tokens: null # AUDIT_MEMORY_MAX_CONTEXT_ITEM_TOKENS + max_trajectory_tokens: null # AUDIT_MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: null # AUDIT_MEMORY_MAX_QUESTION_TOKENS + # ============================================================================ # OUTPUT # ============================================================================ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..002b76a --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,135 @@ +[← Back to README](../README.md#documentation) + +# Architecture + +## Pipeline Overview + +CodeSpy's review pipeline follows a 4-step flow: + +1. **Scope Identifier** (ReAct + tools) — Identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) +2. **Summarizer** (ChainOfThought) — Generates 2-3 sentence PR summary +3. **Parallel Review Modules** — Supply Chain Auditor, Code Reviewer, and Doc Reviewer run simultaneously +4. **Auditor** (ChainOfThought) — Generates quality assessment + recommendation (APPROVE / REQUEST_CHANGES / NEEDS_DISCUSSION) + +## Architecture Diagram + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ codespy CLI │ +├─────────────────────────────────────────────────────────────────────┤ +│ review | review-local | review-uncommitted | serve │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ Git Platform Integration │ +│ GitHub / GitLab — fetch diff, changed files, commit messages │ +│ Auto-detects platform · Sparse checkout for full context │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ DSPy Review Pipeline │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ 1. Scope Identifier (ReAct + tools) │ │ +│ │ Identifies code scopes: frontend, backend, infra, etc. │ │ +│ └──────────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼─────────────────────────────────┐ │ +│ │ 2. Summarizer (ChainOfThought) │ │ +│ │ Generates 2-3 sentence PR summary │ │ +│ └──────────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼─────────────────────────────────┐ │ +│ │ 3. Parallel Review Modules │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ +│ │ │ Supply Chain │ │ Code │ │ Doc │ │ │ +│ │ │ Auditor │ │ Reviewer │ │ Reviewer │ │ │ +│ │ │ (ReAct+tools)│ │ (ReAct+tools)│ │(ChainOfThought│ │ │ +│ │ └──────────────┘ └──────────────┘ └───────────────┘ │ │ +│ └──────────────────────────┬─────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼─────────────────────────────────┐ │ +│ │ 4. Auditor (ChainOfThought) │ │ +│ │ Quality assessment + recommendation │ │ +│ │ (APPROVE / REQUEST_CHANGES / NEEDS_DISCUSSION) │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┐ │ +│ │ Hippocampus Memory (cross-cutting) │ │ +│ │ Episode persistence · Context memory · Distiller/ │ │ +│ │ Cartographer reflection · Topic-based organization │ │ +│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘ │ +│ │ +│ Cost Tracker (tokens, calls, $) │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ Tools Layer │ +│ Filesystem · Git (GH+GL) · Web · Cyber/OSV │ +│ Parsers: Ripgrep (code search) · Tree-sitter (multi-lang AST) │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ LLM Backend (LiteLLM) │ +│ Bedrock | OpenAI | Anthropic | Gemini | Ollama | Any compatible │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## DSPy Signatures + +| Signature | Config Key | Type | Description | +|-----------|------------|------|-------------| +| **ScopeIdentifierSignature** | `scope` | ReAct | Identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) | +| **PRSummarySignature** | `summary` | ChainOfThought | Generates PR summary | +| **CodeReviewSignature** | `code_review` | ReAct | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells | +| **DocReviewSignature** | `doc` | ChainOfThought | Detects stale or wrong documentation caused by code changes | +| **SupplyChainSecuritySignature** | `supply_chain` | ReAct | Analyzes artifacts (Dockerfiles) and dependencies for supply chain security | +| **AuditSignature** | `audit` | ChainOfThought | Generates quality assessment and recommendation | + +See [Configuration](configuration.md) for per-signature settings. + +## Hippocampus Memory + +Episode-based memory that wraps DSPy agents with persistent context across reviews. Agents accumulate knowledge about a codebase scope over time — patterns, constants, parsing schemas, and reuse it in subsequent reviews of the same code area. + +See [Memory System](memory.md) for implementation details. + +## Tools Layer + +- **Filesystem**: `read_file`, `list_dir` +- **Git**: GitHub + GitLab clients, sparse checkout +- **Parsers**: Ripgrep (code search) + Tree-sitter (multi-language AST) +- **Web**: Browser-based web search +- **Cyber/OSV**: Vulnerability scanning + +## Supported Languages + +Tree-sitter based parsing for context-aware analysis: + +| Language | Extensions | Features | +|----------|-----------|----------| +| Bash | `.sh`, `.bash` | Functions, commands | +| C/C++ | `.c`, `.cpp`, `.h`, `.hpp` | Functions, classes, structs | +| C# | `.cs` | Methods, classes, interfaces | +| Go | `.go` | Functions, structs, interfaces | +| Java | `.java` | Methods, classes, packages | +| JavaScript | `.js`, `.jsx` | Functions, classes, imports | +| Kotlin | `.kt` | Functions, classes, objects | +| Objective-C | `.m`, `.h` | Methods, interfaces, protocols | +| PHP | `.php` | Functions, classes, namespaces | +| Python | `.py` | Functions, classes, imports | +| Ruby | `.rb` | Methods, classes, modules | +| Rust | `.rs` | Functions, structs, traits, impl blocks | +| Swift | `.swift` | Functions, classes, structs | +| Terraform | `.tf` | Resources, data sources, modules, variables | +| TypeScript | `.ts`, `.tsx` | Functions, classes, interfaces | + +All languages are supported for security, bug, and documentation analysis. + +## LLM Backend + +LiteLLM routing to Bedrock, OpenAI, Anthropic, Gemini, Ollama, Azure, and any OpenAI-compatible endpoint. + +--- + +[← Back to README](../README.md#documentation) diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..274a7be --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,199 @@ +[← Back to README](../README.md#documentation) + +# Configuration + +Priority: CLI options > Environment Variables > YAML Config > Defaults + +## Setup + +```bash +cp .env.example .env +``` + +## Git Platform Tokens + +### GitHub Token + +Auto-discovered from: +- `GITHUB_TOKEN` or `GH_TOKEN` environment variables +- GitHub CLI (`gh auth token`) +- Git credential helper +- `~/.netrc` file + +Or create a token at https://github.com/settings/tokens with `repo` scope: +```bash +GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx +``` + +To disable auto-discovery: +```bash +GITHUB_AUTO_DISCOVER_TOKEN=false +``` + +### GitLab Token + +Auto-discovered from: +- `GITLAB_TOKEN` or `GITLAB_PRIVATE_TOKEN` environment variables +- GitLab CLI (`glab auth token`) +- Git credential helper +- `~/.netrc` file +- python-gitlab config files (`~/.python-gitlab.cfg`, `/etc/python-gitlab.cfg`) + +Or create a token at https://gitlab.com/-/user_settings/personal_access_tokens with `api` scope: +```bash +GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx +``` + +For self-hosted GitLab: +```bash +GITLAB_URL=https://gitlab.mycompany.com +GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx +``` + +To disable auto-discovery: +```bash +GITLAB_AUTO_DISCOVER_TOKEN=false +``` + +## LLM Provider + +codespy auto-discovers credentials for all providers: + +**Anthropic** (auto-discovers from `$ANTHROPIC_API_KEY`, `~/.config/anthropic/`, `~/.anthropic/`): +```bash +DEFAULT_MODEL=anthropic/claude-opus-4-6 +# Optional - set explicitly or let codespy auto-discover: +# ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx +``` + +**AWS Bedrock** (auto-discovers from `~/.aws/credentials`, AWS CLI, env vars): +```bash +DEFAULT_MODEL=bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0 +AWS_REGION=us-east-1 +# Optional - uses ~/.aws/credentials by default, or set explicitly: +# AWS_ACCESS_KEY_ID=... +# AWS_SECRET_ACCESS_KEY=... +``` + +**OpenAI** (auto-discovers from `$OPENAI_API_KEY`, `~/.config/openai/`, `~/.openai/`): +```bash +DEFAULT_MODEL=openai/gpt-5 +# Optional - set explicitly or let codespy auto-discover: +# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx +``` + +**Google Gemini** (auto-discovers from `$GEMINI_API_KEY`, `$GOOGLE_API_KEY`, gcloud ADC): +```bash +DEFAULT_MODEL=gemini/gemini-2.5-pro +# Optional - set explicitly or let codespy auto-discover: +# GEMINI_API_KEY=xxxxxxxxxxxxxxxxxxxx +``` + +**Local Ollama:** +```bash +DEFAULT_MODEL=ollama/llama3 +``` + +To disable auto-discovery for specific providers: +```bash +AUTO_DISCOVER_AWS=false +AUTO_DISCOVER_OPENAI=false +AUTO_DISCOVER_ANTHROPIC=false +AUTO_DISCOVER_GEMINI=false +``` + +## Model Settings + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| Model | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Primary model for all signatures | +| Reasoning effort | `DEFAULT_REASONING_EFFORT` | `medium` | Provider reasoning budget: `minimal`, `low`, `medium`, `high` | +| Max tokens | `DEFAULT_MAX_TOKENS` | `64000` | Output token budget per completion (reasoning tokens included) | +| Temperature | `DEFAULT_TEMPERATURE` | `1` | Must be 1 while reasoning is enabled | +| Max iterations | `DEFAULT_MAX_ITERS` | `10` | Maximum ReAct iterations for tool-using agents | +| Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) | + +## Recommended Model Strategy + +| Tier | Role | Env Var | Default | Recommended | +|------|------|---------|---------|-------------| +| Smart | Core analysis & reasoning | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Claude Opus / GPT-5 | +| Mid-tier | Field extraction | `EXTRACTION_MODEL` | Falls back to DEFAULT_MODEL | Claude Sonnet | +| Cheap | PR summary | `SUMMARY_MODEL` | Falls back to DEFAULT_MODEL | Claude Haiku | +| Cheap | Memory reflection | `MEMORY_DISTILLER_MODEL` / `MEMORY_CARTOGRAPHER_MODEL` | Falls back to DEFAULT_MODEL | Claude Haiku | + +## Per-Signature Configuration + +Each signature supports env var overrides: `_` + +| Signature | Config Key | Available Settings | +|-----------|------------|-------------------| +| Scope Identifier | `scope` | ENABLED, MAX_ITERS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| PR Summary | `summary` | ENABLED, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Code Reviewer | `code_review` | ENABLED, MAX_ITERS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Doc Reviewer | `doc` | ENABLED, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Supply Chain | `supply_chain` | ENABLED, MAX_ITERS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS, SCAN_UNCHANGED | +| Auditor | `audit` | ENABLED, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | + +Example: `CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929` + +## Advanced Configuration (YAML) + +For per-signature settings, use `codespy.yaml`. See [`codespy.yaml`](../codespy.yaml) for all available options including: +- LLM provider settings and auto-discovery +- Git platform configuration (GitHub/GitLab) +- Per-signature model and iteration overrides +- Output format and destination settings +- Directory exclusions + +Override YAML settings via environment variables using `_` separator: + +```bash +# Default settings +export DEFAULT_MODEL=anthropic/claude-opus-4-6 +export DEFAULT_MAX_ITERS=20 + +# Per-signature settings (use signature name, not module name) +export CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 + +# Output settings +export OUTPUT_STDOUT=false +export OUTPUT_GIT=true +``` + +## Memory Configuration + +Brief overview: + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| Backend | `MEMORY_BACKEND` | `filesystem` | Storage: `filesystem` or `s3` | +| Root path | `MEMORY_ROOT` | `~/.cache/codespy/memory` | Filesystem storage location | +| Default enabled | `MEMORY_DEFAULT_ENABLED` | `false` | Enable memory globally | +| Max reflects | `MEMORY_DEFAULT_MAX_REFLECTS` | `0` | Reflection iterations (0 = once at end) | + +See [Memory System](memory.md) for full memory configuration details. + +## Output Settings + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| Format | `OUTPUT_FORMAT` | `markdown` | `markdown` or `json` | +| Stdout | `OUTPUT_STDOUT` | `true` | Enable stdout output | +| Git | `OUTPUT_GIT` | `true` | Post review to GitHub/GitLab | +| Cache dir | `CACHE_DIR` | `~/.cache/codespy` | Cache directory path | + +## File Exclusions + +`EXCLUDED_DIRECTORIES` (JSON array in env) — Directories to skip during code review. Binary files, lock files, and minified files are always excluded automatically. + +Default excluded directories: +- Vendor/dependency: `vendor`, `node_modules`, `third_party`, `external`, `deps`, `_vendor`, `vendored` +- Build output: `dist`, `build`, `out`, `target` +- Package manager: `.bundle`, `Pods`, `Carthage`, `bower_components`, `jspm_packages` +- Version control: `.git`, `.svn`, `.hg` +- Cache: `__pycache__`, `.cache`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache` + +--- + +[← Back to README](../README.md#documentation) diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..2af2d4e --- /dev/null +++ b/docs/development.md @@ -0,0 +1,63 @@ +[← Back to README](../README.md#documentation) + +# Development + +## Setup + +```bash +# Clone the repository +git clone https://github.com/khezen/codespy.git +cd codespy + +# Quick setup (creates .env and installs dependencies) +make setup + +# Or manually with Poetry: +p poetry install # Install all dependencies including dev +poetry lock # Update lock file +``` + +## Make Targets + +```bash +make help # Show all available targets +make setup # Create .env and install dependencies +make lint # Run ruff linter +make format # Format code with ruff +make typecheck # Run mypy type checker +make test # Run pytest tests +make build # Build package with Poetry +make clean # Clean build artifacts +``` + +## Running Directly + +```bash +# Run codespy via Poetry +poetry run codespy review https://github.com/owner/repo/pull/123 + +# Run linters directly +poetry run ruff check src/ +poetry run mypy src/ +``` + +## Project Structure + +``` +src/codespy/ +├── cli/ # CLI commands and argument parsing +├── config/ # Configuration management +├── git/ # GitHub/GitLab platform clients +├── llm/ # LLM backend and DSPy integration +├── review/ # Review pipeline and signatures +│ ├── agents/ # ReAct and ChainOfThought agents +│ ├── signatures/ # DSPy signature definitions +│ └── tools/ # Agent tools (filesystem, git, web, etc.) +├── memory/ # Hippocampus memory system +├── output/ # Output formatters (markdown, json, git comments) +└── utils/ # Utility functions +``` + +--- + +[← Back to README](../README.md#documentation) diff --git a/docs/memory.md b/docs/memory.md new file mode 100644 index 0000000..d6e05b5 --- /dev/null +++ b/docs/memory.md @@ -0,0 +1,117 @@ +[← Back to README](../README.md#documentation) + +# Hippocampus Memory System + +## Overview + +Episode-based memory that wraps DSPy agents with persistent context across reviews. +Agents accumulate knowledge about a codebase scope over time — patterns, constants, +parsing schemas — and reuse it in subsequent reviews of the same code area. + +## Concepts + +### Topics + +- Every scope gets a `topic_id` derived from `make_topic_id(repo_slug, subroot)` +- Topics organize memory by code area so knowledge doesn't bleed between scopes +- `compute_common_ancestor_topic_id()` finds shared parent for cross-scope queries + +### Episodes + +- An Episode captures one agent's run: task, context_memory, mutations, timestamp +- Stored as JSON at: `/global/episodic///codespy--.json` +- `find_latest_episode()` loads the most recent episode by `modified_at` for a given path prefix + +### Context Memory + +Five sections (from general to specific): + +1. **`context_roadmap`** — High-level codebase structure and navigation hints +2. **`context_understanding`** — Domain knowledge and design patterns observed +3. **`domain_constants`** — Exact values, URLs, identifiers that repeat across reviews +4. **`parsing_schema`** — File format conventions, naming patterns, structural rules +5. **`reusable_results`** — Computed facts reusable in future reviews + +Each section contains Items with tags (general, scope-specific) and text content. + +## Reflection Pipeline + +After each agent run (at `end_episode()`): + +1. **Distiller** — Analyzes the agent's trajectory (head 60% + tail 40%, capped at `max_trajectory_tokens`) and proposes `CacheCandidate` items for context memory +2. **Cartographer** — Takes candidates + current context memory, decides operations: + - `ADD` — Insert new item + - `REPLACE` — Update existing item with new knowledge + - `DELETE` — Remove outdated/irrelevant item +3. **Eviction** — If memory exceeds `max_context_memory_tokens`, oldest general items are evicted first + +Reflection iterates `max_reflects` times (0 = reflect once at end_episode). + +## Token Budgets + +| Budget | Env Var | Default | Purpose | +|--------|---------|---------|---------| +| Context memory | `MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS` | 8192 | Ceiling on persisted ContextMemory (re-sent every iteration) | +| Item | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | 410 | Soft per-item limit (expressed to LLM, not truncated) | +| Trajectory | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | 8192 | Head+tail cap on trajectory fed to Distiller | +| Question | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | 2048 | Cap on serialized inputs as reflection question | + +Item capacity ≈ context_memory_tokens / item_tokens (8192/410 ≈ 19 items) + +## Configuration + +### Global Settings + +| Env Var | YAML Path | Default | Description | +|---------|-----------|---------|-------------| +| `MEMORY_BACKEND` | `memory.backend` | `filesystem` | Storage backend: `filesystem` or `s3` | +| `MEMORY_ROOT` | `memory.root` | `~/.cache/codespy/memory` | Filesystem storage path | +| `MEMORY_S3_BUCKET` | `memory.s3_bucket` | — | S3 bucket name | +| `MEMORY_S3_REGION` | `memory.s3_region` | (aws_region) | S3 region | +| `MEMORY_S3_ENDPOINT_URL` | `memory.s3_endpoint_url` | — | MinIO/S3-compatible endpoint | +| `MEMORY_DEFAULT_ENABLED` | `memory.default_enabled` | `false` | Enable memory globally | +| `MEMORY_DEFAULT_MAX_REFLECTS` | `memory.default_max_reflects` | `0` | Reflection iterations (0 = once at end) | + +### Reflection Module LLM Overrides + +| Module | Env Var Pattern | YAML Path | +|--------|----------------|-----------| +| Distiller | `MEMORY_DISTILLER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS}` | `memory.distiller.*` | +| Cartographer | `MEMORY_CARTOGRAPHER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS}` | `memory.cartographer.*` | + +### Per-Signature Memory Overrides + +Each signature's `memory:` block in YAML (or `_MEMORY_*` env vars): + +| Setting | Env Var Suffix | Description | +|---------|---------------|-------------| +| enabled | `_MEMORY_ENABLED` | Enable/disable memory for this signature | +| max_reflects | `_MEMORY_MAX_REFLECTS` | Override reflection count | +| max_context_memory_tokens | `_MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | Override memory ceiling | +| max_context_item_tokens | `_MEMORY_MAX_CONTEXT_ITEM_TOKENS` | Override item ceiling | +| max_trajectory_tokens | `_MEMORY_MAX_TRAJECTORY_TOKENS` | Override trajectory cap | +| max_question_tokens | `_MEMORY_MAX_QUESTION_TOKENS` | Override question cap | + +Example: `CODE_REVIEW_MEMORY_ENABLED=true` or `SUMMARY_MEMORY_MAX_REFLECTS=2` + +See [Configuration](configuration.md#recommended-model-strategy) for recommended reflection models. + +## Quick Start + +Enable memory for code review: +```bash +MEMORY_DEFAULT_ENABLED=true +# Or per-signature: +CODE_REVIEW_MEMORY_ENABLED=true +SUMMARY_MEMORY_ENABLED=true +``` + +Optimize with cheap reflection model: +```bash +MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 +MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 +``` + +--- + +[← Back to README](../README.md#documentation) diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..4df540c --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,281 @@ +[← Back to README](../README.md#documentation) + +# Usage Guide + +## Command Line + +```bash +# Review GitHub Pull Request +codespy review https://github.com/owner/repo/pull/123 + +# Review GitLab Merge Request +codespy review https://gitlab.com/group/project/-/merge_requests/123 + +# GitLab with nested groups +codespy review https://gitlab.com/group/subgroup/project/-/merge_requests/123 + +# Self-hosted GitLab +codespy review https://gitlab.mycompany.com/team/project/-/merge_requests/123 + +# Output as JSON +codespy review https://github.com/owner/repo/pull/123 --output json + +# Use a specific model +codespy review https://github.com/owner/repo/pull/123 --model anthropic/claude-opus-4-6 + +# Use a custom config file +codespy review https://github.com/owner/repo/pull/123 --config path/to/config.yaml +codespy review https://github.com/owner/repo/pull/123 -f staging.yaml + +# Disable stdout output (useful with --git-comment) +codespy review https://github.com/owner/repo/pull/123 --no-stdout + +# Post review as GitHub/GitLab comment +codespy review https://github.com/owner/repo/pull/123 --git-comment + +# Combine: only post to Git platform, no stdout +codespy review https://github.com/owner/repo/pull/123 --no-stdout --git-comment + +# Show current configuration +codespy config + +# Show configuration from a specific file +codespy config --config path/to/config.yaml + +# Show version +codespy --version + +# Review local git changes (no GitHub/GitLab needed) +codespy review-local # Review current dir vs main +codespy review-local /path/to/repo # Review specific repo +codespy review-local --base develop # Compare against develop +codespy review-local --base origin/main # Compare against origin/main +codespy review-local --base HEAD~5 # Compare against 5 commits back + +# Review uncommitted changes (staged + unstaged) +codespy review-uncommitted # Review current dir +codespy review-uncommitted /path/to/repo +codespy review-uncommitted --output json +``` + +## IDE Integration (MCP Server) + +CodeSpy can run as an MCP (Model Context Protocol) server for integration with AI coding assistants like Cline, enabling code reviews directly from your editor without leaving your workflow. + +```bash +# Start the MCP server +codespy serve + +# Use a custom config file +codespy serve --config path/to/config.yaml +``` + +**Configure your IDE** (example for Cline in VS Code): + +Add to `cline_mcp_settings.json`: +```json +{ + "mcpServers": { + "codespy-reviewer": { + "command": "codespy", + "args": ["serve"], + "env": { + "DEFAULT_MODEL": "anthropic/claude-opus-4-6", + "ANTHROPIC_API_KEY": "your-key-here" + } + } + } +} +``` + +Or for AWS Bedrock: +```json +{ + "mcpServers": { + "codespy-reviewer": { + "command": "codespy", + "args": ["serve"], + "env": { + "DEFAULT_MODEL": "bedrock/us.anthropic.claude-opus-4-6-v1", + "AWS_REGION": "us-east-1", + "AWS_ACCESS_KEY_ID": "your-access-key", + "AWS_SECRET_ACCESS_KEY": "your-secret-key" + } + } + } +} +``` + +**Available MCP Tools:** +- `review_local_changes(repo_path, base_ref)` — Review branch changes vs base (e.g., vs `main`) +- `review_uncommitted(repo_path)` — Review staged + unstaged working tree changes +- `review_pr(mr_url)` — Review a GitHub PR or GitLab MR by URL + +Then ask your AI assistant: *"Review my local changes"* or *"Review uncommitted work in /path/to/repo"* + +## Docker + +```bash +# With docker run (using GHCR image) +docker run --rm \ + -e GITHUB_TOKEN=$GITHUB_TOKEN \ + -e DEFAULT_MODEL=anthropic/claude-opus-4-6 \ + -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ + ghcr.io/khezen/codespy:latest review https://github.com/owner/repo/pull/123 + +# Or use a specific version +docker run --rm \ + -e GITHUB_TOKEN=$GITHUB_TOKEN \ + -e DEFAULT_MODEL=anthropic/claude-opus-4-6 \ + -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \ + ghcr.io/khezen/codespy:0.2.1 review https://github.com/owner/repo/pull/123 +``` + +## GitHub Action + +Add CodeSpy to your repository for automatic PR reviews: + +**Trigger on `/codespy review` comment:** + +```yaml +# .github/workflows/codespy-review.yml +name: CodeSpy Code Review + +on: + issue_comment: + types: [created] + +jobs: + review: + # Only run on PR comments containing '/codespy review' + if: | + github.event.issue.pull_request && + contains(github.event.comment.body, '/codespy review') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Run CodeSpy Review + uses: khezen/codespy@v1 + with: + model: 'anthropic/claude-opus-4-6' + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +**Trigger automatically on every PR:** + +```yaml +# .github/workflows/codespy-review.yml +name: CodeSpy Code Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Run CodeSpy Review + uses: khezen/codespy@v1 + with: + model: 'anthropic/claude-opus-4-6' + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +See [`../.github/workflows/codespy-review.yml.example`](../.github/workflows/codespy-review.yml.example) for more examples. + +## Output + +### Markdown (default) + +```markdown +# Code Review: Add user authentication + +**PR:** [owner/repo#123](https://github.com/owner/repo/pull/123) +**Reviewed at:** 2024-01-15 10:30 UTC +**Model:** anthropic/claude-opus-4-6 + +## Summary + +This PR implements user authentication with JWT tokens... + +## Statistics + +- **Total Issues:** 3 +- **Critical:** 1 +- **Security:** 1 +- **Bugs:** 1 +- **Documentation:** 1 + +## Issues + +### 🔴 Critical (1) + +#### SQL Injection Vulnerability + +**Location:** `src/auth/login.py:45` +**Category:** security + +The user input is directly interpolated into the SQL query... + +**Code:** +query = f"SELECT * FROM users WHERE username = '{username}'" + +**Suggestion:** +Use parameterized queries instead... + +**Reference:** [CWE-89](https://cwe.mitre.org/data/definitions/89.html) +``` + +### GitHub/GitLab Review Comments + +CodeSpy can post reviews directly to GitHub PRs or GitLab MRs as native review comments with inline annotations. + +**Enable via CLI:** +```bash +# GitHub +codespy review https://github.com/owner/repo/pull/123 --git-comment + +# GitLab +codespy review https://gitlab.com/group/project/-/merge_requests/123 --git-comment + +# Combine: only post to platform, no stdout +codespy review https://github.com/owner/repo/pull/123 --no-stdout --git-comment +``` + +**Enable via configuration:** +```bash +# Environment variable +export OUTPUT_GIT=true + +# Or in codespy.yaml +output_git: true +``` + +**Features:** + +- 🎯 **Inline Comments** - Issues are posted as review comments on the exact lines where they occur +- 📏 **Multi-line Support** - Issues spanning multiple lines are annotated with start/end line ranges +- 🔴🟠🟡🔵 **Severity Indicators** - Visual emoji markers for Critical, High, Medium, Low severity +- 📦 **Collapsible Sections** - Organized review body with expandable details: + - 📋 Summary of changes + - 🎯 Quality Assessment + - 📊 Statistics table + - 💰 Cost breakdown per signature + - 💡 Recommendation +- 🔗 **CWE References** - Security issues link directly to MITRE CWE database + +--- + +See [Configuration](configuration.md) for all available settings. + +--- + +[← Back to README](../README.md#documentation) From d5277453ad7652be7d0884847aa42419e91a1fef Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 01:10:38 +0200 Subject: [PATCH 65/79] wip --- .env.example | 25 ------------- codespy.yaml | 25 ------------- docs/development.md | 42 +++++++++++++++------ docs/memory.md | 4 -- src/codespy/config.py | 76 ++++---------------------------------- src/codespy/config_dspy.py | 4 -- 6 files changed, 37 insertions(+), 139 deletions(-) diff --git a/.env.example b/.env.example index 9a635a5..c4269f5 100644 --- a/.env.example +++ b/.env.example @@ -257,10 +257,6 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # - MAX_TOKENS (integer) - Output token budget (unset -> DEFAULT_MAX_TOKENS) # - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) # - MEMORY_MAX_REFLECTS (integer) - Max mid-run reflections (0 = reflect once at end) -# - MEMORY_MAX_CONTEXT_MEMORY_TOKENS (integer) - Ceiling on the persisted ContextMemory -# - MEMORY_MAX_CONTEXT_ITEM_TOKENS (integer) - Budget for a single context-memory item -# - MEMORY_MAX_TRAJECTORY_TOKENS (integer) - Cap on trajectory tokens fed to reflection -# - MEMORY_MAX_QUESTION_TOKENS (integer) - Cap on serialized inputs used as the question # Examples: # CODE_REVIEW_ENABLED=true @@ -272,10 +268,6 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # CODE_REVIEW_MEMORY_ENABLED=true # CODE_REVIEW_MEMORY_MAX_REFLECTS=1 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 -# CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 -# CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS=2048 # SUPPLY_CHAIN_ENABLED=true # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests @@ -283,19 +275,11 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_SCAN_UNCHANGED=false # SUPPLY_CHAIN_MEMORY_ENABLED=true # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 -# SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 -# SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS=2048 # DOC_ENABLED=true # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 -# DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 -# DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 -# DOC_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# DOC_MEMORY_MAX_QUESTION_TOKENS=2048 # SCOPE_ENABLED=true # SCOPE_MAX_ITERS=10 @@ -303,18 +287,9 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SCOPE_MAX_TOKENS=64000 # SCOPE_MEMORY_ENABLED=true # SCOPE_MEMORY_MAX_REFLECTS=1 -# SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 -# SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 -# SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# Unused: scope uses question_field="mr_title", so no inputs are serialized. -# SCOPE_MEMORY_MAX_QUESTION_TOKENS= # SUMMARY_ENABLED=true # SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 # SUMMARY_MEMORY_ENABLED=true # SUMMARY_MEMORY_MAX_REFLECTS=1 -# SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS=8192 -# SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS=410 -# SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS=8192 -# SUMMARY_MEMORY_MAX_QUESTION_TOKENS=2048 diff --git a/codespy.yaml b/codespy.yaml index 51bb75a..1dd1c4a 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -205,10 +205,6 @@ signatures: memory: enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # SUPPLY_CHAIN_MEMORY_MAX_QUESTION_TOKENS # Code Reviewer signature (bugs, security, removed defensive code, code smells) # Unified code review: bugs, security vulnerabilities, and code smells in a single agent pass per scope @@ -222,10 +218,6 @@ signatures: memory: enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # CODE_REVIEW_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # CODE_REVIEW_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # CODE_REVIEW_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # CODE_REVIEW_MEMORY_MAX_QUESTION_TOKENS # Documentation Reviewer signature (compares patches against extracted documentation) # Note: doc extraction is now deterministic (no LLM) — see doc_extractor.py @@ -238,10 +230,6 @@ signatures: memory: enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # DOC_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # DOC_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # DOC_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # DOC_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # DOC_MEMORY_MAX_QUESTION_TOKENS # Scope Identifier signature scope: @@ -254,11 +242,6 @@ signatures: memory: enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SCOPE_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # SCOPE_MEMORY_MAX_TRAJECTORY_TOKENS - # Unused: scope passes question_field="mr_title", so no inputs are serialized. - max_question_tokens: null # SCOPE_MEMORY_MAX_QUESTION_TOKENS # Summarizer signature summary: @@ -270,10 +253,6 @@ signatures: memory: enabled: true # SUMMARY_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # SUMMARY_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # SUMMARY_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # SUMMARY_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # SUMMARY_MEMORY_MAX_QUESTION_TOKENS # Auditor signature (quality assessment + recommendation after all reviews) audit: @@ -285,10 +264,6 @@ signatures: memory: enabled: null # AUDIT_MEMORY_ENABLED (null -> memory.default_enabled) max_reflects: null # AUDIT_MEMORY_MAX_REFLECTS - max_context_memory_tokens: null # AUDIT_MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: null # AUDIT_MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: null # AUDIT_MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: null # AUDIT_MEMORY_MAX_QUESTION_TOKENS # ============================================================================ # OUTPUT diff --git a/docs/development.md b/docs/development.md index 2af2d4e..5df5633 100644 --- a/docs/development.md +++ b/docs/development.md @@ -13,7 +13,7 @@ cd codespy make setup # Or manually with Poetry: -p poetry install # Install all dependencies including dev +poetry install # Install all dependencies including dev poetry lock # Update lock file ``` @@ -45,17 +45,35 @@ poetry run mypy src/ ``` src/codespy/ -├── cli/ # CLI commands and argument parsing -├── config/ # Configuration management -├── git/ # GitHub/GitLab platform clients -├── llm/ # LLM backend and DSPy integration -├── review/ # Review pipeline and signatures -│ ├── agents/ # ReAct and ChainOfThought agents -│ ├── signatures/ # DSPy signature definitions -│ └── tools/ # Agent tools (filesystem, git, web, etc.) -├── memory/ # Hippocampus memory system -├── output/ # Output formatters (markdown, json, git comments) -└── utils/ # Utility functions +├── __init__.py +├── cli.py # Main CLI entrypoint +├── cli_local.py # Local review commands +├── cli_remote.py # Remote PR/MR review commands +├── cli_mcp_server.py # MCP server command +├── config.py # Main configuration +├── config_dspy.py # DSPy configuration +├── config_git.py # Git platform configuration +├── config_io.py # I/O configuration +├── config_llm.py # LLM provider configuration +├── config_memory.py # Memory system configuration +├── agents/ # DSPy agents and pipeline +│ ├── cost_tracker.py # Token/cost tracking +│ ├── dspy_config.py # DSPy runtime config +│ ├── memory/ # Hippocampus memory system +│ │ └── hippocampus/ # Episode persistence, context memory, budget +│ └── reviewer/ # Review pipeline +│ ├── models.py # Review data models +│ ├── reviewer.py # Main review orchestrator +│ ├── server.py # MCP server implementation +│ ├── modules/ # Pipeline stages (scope_resolver, summarizer, code_reviewer, doc_reviewer, supply_chain_auditor, auditor) +│ └── reporters/ # Output reporters (git comments, stdout) +└── tools/ # Agent tools + ├── git/ # GitHub/GitLab clients, local diff, patch utils + ├── cyber/osv/ # OSV vulnerability scanning + ├── parsers/ # Ripgrep + Tree-sitter + ├── storage/ # Filesystem + S3 + ├── web/ # Web search client + └── mcp_utils.py # MCP tool utilities ``` --- diff --git a/docs/memory.md b/docs/memory.md index d6e05b5..bd4b07a 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -87,10 +87,6 @@ Each signature's `memory:` block in YAML (or `_MEMORY_*` env vars): |---------|---------------|-------------| | enabled | `_MEMORY_ENABLED` | Enable/disable memory for this signature | | max_reflects | `_MEMORY_MAX_REFLECTS` | Override reflection count | -| max_context_memory_tokens | `_MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | Override memory ceiling | -| max_context_item_tokens | `_MEMORY_MAX_CONTEXT_ITEM_TOKENS` | Override item ceiling | -| max_trajectory_tokens | `_MEMORY_MAX_TRAJECTORY_TOKENS` | Override trajectory cap | -| max_question_tokens | `_MEMORY_MAX_QUESTION_TOKENS` | Override question cap | Example: `CODE_REVIEW_MEMORY_ENABLED=true` or `SUMMARY_MEMORY_MAX_REFLECTS=2` diff --git a/src/codespy/config.py b/src/codespy/config.py index 78a13e8..f2bb9da 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -260,81 +260,19 @@ def get_memory_max_reflects(self, signature_name: str) -> int | None: else self.memory.default_max_reflects ) - def get_memory_max_context_memory_tokens(self, signature_name: str) -> int: - """Get max_context_memory_tokens for a signature's memory (signature-specific or default). - - Bounds the rendered ContextMemory — the persisted artifact that is prepended - to every predictor of the wrapped agent, and therefore re-sent on every - ReAct iteration. - """ - config = self.get_signature_config(signature_name).memory - return ( - config.max_context_memory_tokens - if config.max_context_memory_tokens is not None - else self.memory.default_max_context_memory_tokens - ) - - def get_memory_max_context_item_tokens(self, signature_name: str) -> int: - """Get max_context_item_tokens for a signature's memory (signature-specific or default). - - Bounds a *single* context-memory item. Handed to the Distiller and the - Cartographer as a prompt input so they keep each item compact instead of - spending the whole memory budget on one verbose entry. Soft limit — the hard, - memory-wide ceiling is ``get_memory_max_context_memory_tokens``. - """ - config = self.get_signature_config(signature_name).memory - return ( - config.max_context_item_tokens - if config.max_context_item_tokens is not None - else self.memory.default_max_context_item_tokens - ) - - def get_memory_max_trajectory_tokens(self, signature_name: str) -> int | None: - """Get max_trajectory_tokens for a signature's memory (signature-specific or default). - - Bounds the agent trajectory fed to the Distiller. - """ - config = self.get_signature_config(signature_name).memory - return ( - config.max_trajectory_tokens - if config.max_trajectory_tokens is not None - else self.memory.default_max_trajectory_tokens - ) - - def get_memory_max_question_tokens(self, signature_name: str) -> int | None: - """Get max_question_tokens for a signature's memory (signature-specific or default). - - Bounds the serialized agent inputs used as the reflection "question". - Ignored when the caller passes an explicit ``question`` string. - """ - config = self.get_signature_config(signature_name).memory - return ( - config.max_question_tokens - if config.max_question_tokens is not None - else self.memory.default_max_question_tokens - ) - def get_memory_budget(self, signature_name: str) -> "MemoryBudget": - """Resolve the full ``MemoryBudget`` for a signature's memory. - - Composes the four per-field getters, so each budget still resolves as - "signature-specific override, else ``memory.default_*``". Pass the result - straight to ``Hippocampus(module, budget=...)``. + """Resolve the ``MemoryBudget`` for a signature. - Args: - signature_name: The signature whose memory budget to resolve. - - Returns: - A fully resolved ``MemoryBudget`` (no None-means-default fields). + Token budgets are global (``memory.default_*``); only ``enabled`` and + ``max_reflects`` support per-signature overrides. """ - # Deferred import — see the TYPE_CHECKING note at the top of this module. from codespy.agents.memory.hippocampus.budget import MemoryBudget return MemoryBudget( - max_context_memory_tokens=self.get_memory_max_context_memory_tokens(signature_name), - max_context_item_tokens=self.get_memory_max_context_item_tokens(signature_name), - max_trajectory_tokens=self.get_memory_max_trajectory_tokens(signature_name), - max_question_tokens=self.get_memory_max_question_tokens(signature_name), + max_context_memory_tokens=self.memory.default_max_context_memory_tokens, + max_context_item_tokens=self.memory.default_max_context_item_tokens, + max_trajectory_tokens=self.memory.default_max_trajectory_tokens, + max_question_tokens=self.memory.default_max_question_tokens, ) def log_signature_configs(self) -> None: diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index b03f91b..1d5da1d 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -26,10 +26,6 @@ class MemorySignatureConfig(BaseModel): enabled: bool | None = None # _MEMORY_ENABLED max_reflects: int | None = None # _MEMORY_MAX_REFLECTS - max_context_memory_tokens: int | None = None # _MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: int | None = None # _MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_trajectory_tokens: int | None = None # _MEMORY_MAX_TRAJECTORY_TOKENS - max_question_tokens: int | None = None # _MEMORY_MAX_QUESTION_TOKENS class SignatureConfig(BaseModel): From de9b6e8883e055b938d59b49526ab358233493f0 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 14:41:57 +0200 Subject: [PATCH 66/79] deny more --- codespy.yaml | 22 +++++++++++++++++++--- src/codespy/config_dspy.py | 2 -- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/codespy.yaml b/codespy.yaml index 1dd1c4a..931984a 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -77,9 +77,25 @@ memory: root: ~/.cache/codespy/memory # MEMORY_ROOT # S3 backend (used when backend: s3) - s3_bucket: null # MEMORY_S3_BUCKET - s3_region: null # MEMORY_S3_REGION (falls back to aws_region) - s3_endpoint_url: null # MEMORY_S3_ENDPOINT_URL (for MinIO/S3-compatible) + # --------------------------------------------------------------------------- + # Use this to store memory episodes in S3-compatible object storage instead of + # the local filesystem. Useful for: + # - Shared memory across multiple codespy instances (CI/CD, distributed setups) + # - Centralized episode storage + # - Integration with MinIO, AWS S3, or other S3-compatible services + # --------------------------------------------------------------------------- + s3_bucket: null # MEMORY_S3_BUCKET - The bucket name + # Example: "codespy-memory" or "mycompany-codespy" + + s3_region: null # MEMORY_S3_REGION - AWS region for the bucket + # Falls back to aws_region (from llm.* section above) + # Example: "us-east-1", "eu-west-1" + + s3_endpoint_url: null # MEMORY_S3_ENDPOINT_URL - Custom endpoint + # Only needed for MinIO or non-AWS S3-compatible storage + # Leave as null for AWS S3 + # Example: "http://localhost:9000" (MinIO local) + # Example: "https://minio.example.com" (MinIO remote) # Reflection defaults — overridable per-signature via signatures..memory default_enabled: false # MEMORY_DEFAULT_ENABLED diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index 1d5da1d..db61b37 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -90,8 +90,6 @@ def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - ``CODE_REVIEW_MAX_ITERS`` -> signatures.code_review.max_iters - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - - ``SCOPE_MEMORY_MAX_CONTEXT_MEMORY_TOKENS`` -> signatures.scope.memory.max_context_memory_tokens - - ``SCOPE_MEMORY_MAX_CONTEXT_ITEM_TOKENS`` -> signatures.scope.memory.max_context_item_tokens Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) are handled directly by pydantic-settings and should NOT be processed here. From 7b64d76aef4cdca155fed0f12f65754178c27782 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 15:35:18 +0200 Subject: [PATCH 67/79] wip --- docs/architecture.md | 4 ++-- docs/configuration.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 002b76a..85de71e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,7 +7,7 @@ CodeSpy's review pipeline follows a 4-step flow: 1. **Scope Identifier** (ReAct + tools) — Identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) -2. **Summarizer** (ChainOfThought) — Generates 2-3 sentence PR summary +2. **PR Summary** (ChainOfThought) — Generates 2-3 sentence PR summary 3. **Parallel Review Modules** — Supply Chain Auditor, Code Reviewer, and Doc Reviewer run simultaneously 4. **Auditor** (ChainOfThought) — Generates quality assessment + recommendation (APPROVE / REQUEST_CHANGES / NEEDS_DISCUSSION) @@ -35,7 +35,7 @@ CodeSpy's review pipeline follows a 4-step flow: │ └──────────────────────────┬─────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────▼─────────────────────────────────┐ │ -│ │ 2. Summarizer (ChainOfThought) │ │ +│ │ 2. PR Summary (ChainOfThought) │ │ │ │ Generates 2-3 sentence PR summary │ │ │ └──────────────────────────┬─────────────────────────────────┘ │ │ │ │ diff --git a/docs/configuration.md b/docs/configuration.md index 274a7be..462ae95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -171,6 +171,10 @@ Brief overview: | Root path | `MEMORY_ROOT` | `~/.cache/codespy/memory` | Filesystem storage location | | Default enabled | `MEMORY_DEFAULT_ENABLED` | `false` | Enable memory globally | | Max reflects | `MEMORY_DEFAULT_MAX_REFLECTS` | `0` | Reflection iterations (0 = once at end) | +| Context memory tokens | `MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS` | `8192` | Max tokens for persisted context memory | +| Item tokens | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | `410` | Soft per-item token limit | +| Trajectory tokens | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | `8192` | Cap on trajectory fed to Distiller | +| Question tokens | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | `2048` | Cap on serialized reflection inputs | See [Memory System](memory.md) for full memory configuration details. From 98f0a1f99443b60e3350b6f3885ec5bbd73c8e48 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 15:43:37 +0200 Subject: [PATCH 68/79] add access verification --- src/codespy/agents/reviewer/reviewer.py | 12 + src/codespy/config_memory.py | 28 +++ src/codespy/tools/storage/base.py | 12 + .../tools/storage/filesystem/client.py | 16 ++ src/codespy/tools/storage/s3/client.py | 10 + tests/test_config_memory.py | 219 ++++++++++++++++++ 6 files changed, 297 insertions(+) create mode 100644 tests/test_config_memory.py diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 6ca75f4..ed1d72a 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -10,6 +10,7 @@ from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access from codespy.config import Settings, get_settings +from codespy.config_memory import verify_memory_access from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest from codespy.tools.git.local_diff import build_mr_from_diff from codespy.tools.git.patch_utils import compact_patches @@ -65,6 +66,14 @@ def _verify_model_access(self) -> None: raise ValueError(f"Model access failed: {message}") logger.info(f"Model access: {message}") + def _verify_memory_access(self) -> None: + """Verify memory storage access.""" + logger.info("Verifying memory storage access...") + success, message = verify_memory_access(self.settings) + if not success: + raise ValueError(f"Memory storage access failed: {message}") + logger.info(f"Memory storage: {message}") + def _get_git_client(self, url: str) -> GitClient: """Get or create a Git client for the given URL.""" if self._git_client is None: @@ -179,6 +188,9 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Always verify model access self._verify_model_access() + # Verify memory storage access + self._verify_memory_access() + # Determine mode and fetch/build MR accordingly if isinstance(config, RemoteReviewConfig): # Remote mode: fetch from GitHub/GitLab diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 8a7b4fc..9aa7739 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -308,3 +308,31 @@ def reset_memory_store() -> None: global _store, _store_built _store = None _store_built = False + + +def verify_memory_access(settings: Settings) -> tuple[bool, str]: + """Verify memory storage is accessible when memory is active. + + Returns: + Tuple of (success, message). Success is True when memory is disabled + (no active signatures use it) or when the storage backend responds. + """ + from codespy.config_dspy import SIGNATURE_NAMES + + # Skip if no enabled signature uses memory + if not any( + settings.is_signature_enabled(sig) and settings.get_memory_enabled(sig) + for sig in SIGNATURE_NAMES + ): + return True, "Memory disabled — skipping storage check" + + store = get_memory_store(settings) + if store is None: + return False, "Memory is enabled but storage is not configured (missing S3 bucket?)" + + try: + store.verify_access() + except Exception as e: + return False, f"Memory storage not accessible: {e}" + + return True, f"Memory storage verified ({settings.memory.backend})" diff --git a/src/codespy/tools/storage/base.py b/src/codespy/tools/storage/base.py index 14ff3af..b46f327 100644 --- a/src/codespy/tools/storage/base.py +++ b/src/codespy/tools/storage/base.py @@ -37,6 +37,18 @@ def exists(self, path: str = "") -> bool: """ ... + @abstractmethod + def verify_access(self) -> None: + """Verify the storage backend is accessible. + + Raises: + FileNotFoundError: If the storage root does not exist. + NotADirectoryError: If the storage root is not a directory. + PermissionError: If the storage root is not readable. + Exception: Backend-specific errors (e.g., S3 credentials, bucket access). + """ + ... + @abstractmethod def get_info(self, path: str = "") -> Info: """Get metadata about a file or directory. diff --git a/src/codespy/tools/storage/filesystem/client.py b/src/codespy/tools/storage/filesystem/client.py index df0380b..3c0797f 100644 --- a/src/codespy/tools/storage/filesystem/client.py +++ b/src/codespy/tools/storage/filesystem/client.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import os from datetime import datetime, timezone from pathlib import Path @@ -95,6 +96,21 @@ def exists(self, path: str = "") -> bool: except ValueError: return False + def verify_access(self) -> None: + """Verify the filesystem root is accessible. + + Raises: + FileNotFoundError: If the memory root does not exist. + NotADirectoryError: If the memory root is not a directory. + PermissionError: If the memory root is not readable. + """ + if not self.root.exists(): + raise FileNotFoundError(f"Memory root does not exist: {self.root}") + if not self.root.is_dir(): + raise NotADirectoryError(f"Memory root is not a directory: {self.root}") + if not os.access(self.root, os.R_OK): + raise PermissionError(f"Memory root is not readable: {self.root}") + def get_info(self, path: str = "") -> Info: """Get information about a file or directory.""" resolved = self._resolve_path(path) diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index e2eea38..632f579 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -82,6 +82,16 @@ def _file_extension(self, path: str) -> str: return name.rsplit(".", 1)[-1] return "" + def verify_access(self) -> None: + """Verify S3 bucket is accessible (credentials + bucket existence). + + Raises: + ClientError: If the bucket doesn't exist, credentials are invalid, + or IAM lacks ListBucket permission. Let the caller handle this. + """ + # Let ClientError propagate — NoSuchBucket, AccessDenied, InvalidAccessKeyId, etc. + self._s3.list_objects_v2(Bucket=self.bucket, MaxKeys=1) + def _client_error_code(self, exc: Exception) -> str: try: return exc.response["Error"]["Code"] # type: ignore[attr-defined] diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py new file mode 100644 index 0000000..522694a --- /dev/null +++ b/tests/test_config_memory.py @@ -0,0 +1,219 @@ +"""Tests for memory storage access verification.""" + +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from codespy.config_dspy import SIGNATURE_NAMES +from codespy.config_memory import verify_memory_access +from codespy.tools.storage.filesystem.client import FileSystem +from codespy.tools.storage.s3.client import S3Client + + +class TestVerifyMemoryAccess: + """Tests for verify_memory_access function.""" + + def test_verify_memory_access_all_disabled(self): + """When all signatures disabled, returns success with skip message.""" + settings = MagicMock() + settings.is_signature_enabled.return_value = False + settings.get_memory_enabled.return_value = False + + success, message = verify_memory_access(settings) + + assert success is True + assert "Memory disabled" in message + + def test_verify_memory_access_enabled_sig_disabled(self): + """When signature has memory enabled but signature itself is disabled.""" + settings = MagicMock() + + def is_enabled(sig): + return False # All signatures disabled + + def memory_enabled(sig): + return True # But memory is configured + + settings.is_signature_enabled.side_effect = is_enabled + settings.get_memory_enabled.side_effect = memory_enabled + + success, message = verify_memory_access(settings) + + assert success is True + assert "Memory disabled" in message + + def test_verify_memory_access_store_none(self): + """When memory active but store is None (missing S3 bucket).""" + settings = MagicMock() + settings.memory.backend = "s3" + + def is_enabled(sig): + return sig == "summary" # Only summary enabled + + def memory_enabled(sig): + return sig == "summary" # Memory enabled for summary + + settings.is_signature_enabled.side_effect = is_enabled + settings.get_memory_enabled.side_effect = memory_enabled + + with patch("codespy.config_memory.get_memory_store", return_value=None): + success, message = verify_memory_access(settings) + + assert success is False + assert "not configured" in message + + def test_verify_memory_access_filesystem_ok(self, tmp_path): + """When memory active with valid filesystem store.""" + settings = MagicMock() + settings.memory.backend = "filesystem" + + def is_enabled(sig): + return sig == "summary" + + def memory_enabled(sig): + return sig == "summary" + + settings.is_signature_enabled.side_effect = is_enabled + settings.get_memory_enabled.side_effect = memory_enabled + + # Create a real FileSystem with tmp_path + fs = FileSystem(tmp_path) + + with patch("codespy.config_memory.get_memory_store", return_value=fs): + success, message = verify_memory_access(settings) + + assert success is True + assert "verified" in message + assert "filesystem" in message + + def test_verify_memory_access_s3_ok(self): + """When memory active with S3 store that verifies successfully.""" + settings = MagicMock() + settings.memory.backend = "s3" + + def is_enabled(sig): + return sig == "summary" + + def memory_enabled(sig): + return sig == "summary" + + settings.is_signature_enabled.side_effect = is_enabled + settings.get_memory_enabled.side_effect = memory_enabled + + # Mock S3 client that verifies successfully + mock_store = MagicMock() + mock_store.verify_access.return_value = None + + with patch("codespy.config_memory.get_memory_store", return_value=mock_store): + success, message = verify_memory_access(settings) + + assert success is True + assert "verified" in message + assert "s3" in message + mock_store.verify_access.assert_called_once() + + def test_verify_memory_access_raises(self): + """When store's verify_access raises an exception.""" + settings = MagicMock() + settings.memory.backend = "filesystem" + + def is_enabled(sig): + return sig == "summary" + + def memory_enabled(sig): + return sig == "summary" + + settings.is_signature_enabled.side_effect = is_enabled + settings.get_memory_enabled.side_effect = memory_enabled + + # Mock store that raises on verify_access + mock_store = MagicMock() + mock_store.verify_access.side_effect = PermissionError("Access denied") + + with patch("codespy.config_memory.get_memory_store", return_value=mock_store): + success, message = verify_memory_access(settings) + + assert success is False + assert "not accessible" in message + assert "Access denied" in message + + +class TestFileSystemVerifyAccess: + """Tests for FileSystem.verify_access method.""" + + def test_verify_access_filesystem_valid(self, tmp_path): + """Valid filesystem root - no exception raised.""" + fs = FileSystem(tmp_path) + # Should not raise + fs.verify_access() + + def test_verify_access_filesystem_deleted_root(self, tmp_path): + """Root deleted after initialization - raises FileNotFoundError.""" + fs = FileSystem(tmp_path) + # Delete the root directory + import shutil + + shutil.rmtree(tmp_path) + + with pytest.raises(FileNotFoundError, match="does not exist"): + fs.verify_access() + + @pytest.mark.skipif(sys.platform == "win32", reason="chmod 000 not supported on Windows") + def test_verify_access_filesystem_not_readable(self, tmp_path): + """Root not readable - raises PermissionError.""" + import os + + fs = FileSystem(tmp_path) + # Remove read permission + os.chmod(tmp_path, 0o000) + + try: + with pytest.raises(PermissionError, match="not readable"): + fs.verify_access() + finally: + # Restore permission for cleanup + os.chmod(tmp_path, 0o755) + + +class TestS3ClientVerifyAccess: + """Tests for S3Client.verify_access method.""" + + def test_verify_access_s3_success(self): + """Successful S3 access - no exception raised.""" + # Create S3Client without calling __init__ (no boto3) + client = S3Client.__new__(S3Client) + client.bucket = "test-bucket" + client._s3 = MagicMock() + client._s3.list_objects_v2.return_value = {} + + # Should not raise + client.verify_access() + + # Verify the call was made correctly + client._s3.list_objects_v2.assert_called_once_with(Bucket="test-bucket", MaxKeys=1) + + def test_verify_access_s3_client_error(self): + """S3 client error propagates.""" + from unittest.mock import MagicMock + + # Create S3Client without calling __init__ + client = S3Client.__new__(S3Client) + client.bucket = "test-bucket" + client._s3 = MagicMock() + + # Simulate boto3 ClientError + class ClientError(Exception): + def __init__(self, error_response, operation_name): + self.response = error_response + self.operation_name = operation_name + super().__init__(str(error_response)) + + client._s3.list_objects_v2.side_effect = ClientError( + {"Error": {"Code": "NoSuchBucket", "Message": "The specified bucket does not exist"}}, + "ListObjectsV2", + ) + + with pytest.raises(ClientError): + client.verify_access() From 221ec66f170d5ada31f8c8f140f7c737e5bbdb39 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 16:15:03 +0200 Subject: [PATCH 69/79] wip --- docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture.md b/docs/architecture.md index 85de71e..a56eb44 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,7 +79,7 @@ CodeSpy's review pipeline follows a 4-step flow: | Signature | Config Key | Type | Description | |-----------|------------|------|-------------| -| **ScopeIdentifierSignature** | `scope` | ReAct | Identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) | +| **ScopeRefinementSignature** | `scope` | ReAct | Identifies code scopes (frontend, backend, infra, microservice in monorepo, etc.) | | **PRSummarySignature** | `summary` | ChainOfThought | Generates PR summary | | **CodeReviewSignature** | `code_review` | ReAct | Detects verified bugs, security vulnerabilities, removed defensive code, and code smells | | **DocReviewSignature** | `doc` | ChainOfThought | Detects stale or wrong documentation caused by code changes | From 810efaf1a6497013010482b920c6d55d8424919d Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 16:49:50 +0200 Subject: [PATCH 70/79] wip --- src/codespy/agents/reviewer/models.py | 40 +++++++---- .../agents/reviewer/modules/code_reviewer.py | 42 +++++------ .../agents/reviewer/modules/doc_reviewer.py | 41 +++++------ .../agents/reviewer/modules/scope_resolver.py | 72 ++++++++----------- .../reviewer/modules/supply_chain_auditor.py | 47 ++++++------ src/codespy/agents/reviewer/reviewer.py | 42 +++-------- 6 files changed, 121 insertions(+), 163 deletions(-) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index fe5bdda..c2fec34 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -23,18 +23,6 @@ class PRContext(BaseModel): summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") -class ReviewContext(BaseModel): - """Evolving pipeline state threaded through review stages. - - Carries both the immutable PR identity and the inherited context memory - from upstream pipeline stages. Updated at each stage boundary - so downstream modules inherit accumulated understanding. - """ - - pr_context: PRContext = Field(description="Immutable PR identity (repo, number, title, summary)") - memory: ContextMemory | None = Field(default=None, description="Inherited context memory from upstream stages") - - class IssueSeverity(str, Enum): """Severity level of an issue.""" @@ -77,7 +65,33 @@ class PackageManifest(BaseModel): package_name: str | None = Field(default=None, description="Package identity from manifest") -from codespy.tools.git.models import ChangedFile +from codespy.tools.git.models import ChangedFile, MergeRequest + + +class ReviewMetadata(BaseModel): + """Runtime pipeline state, stable once constructed at pipeline start. + + Groups repo_path, run_id, mr, and is_local to reduce parameter + proliferation across module method signatures. + """ + + repo_path: Path + run_id: str | None = None + mr: MergeRequest | None = None + is_local: bool = False + + +class ReviewContext(BaseModel): + """Evolving pipeline state threaded through review stages. + + Carries both the immutable PR identity and the inherited context memory + from upstream pipeline stages. Updated at each stage boundary + so downstream modules inherit accumulated understanding. + """ + + pr_context: PRContext = Field(description="Immutable PR identity (repo, number, title, summary)") + memory: ContextMemory | None = Field(default=None, description="Inherited context memory from upstream stages") + metadata: ReviewMetadata | None = Field(default=None, description="Runtime pipeline state") class ScopeResult(BaseModel): diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 82c8527..554a86b 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -183,27 +183,26 @@ async def _create_tools( async def aforward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for defects, security issues, and code smells. Args: scopes: List of identified scopes with their changed files - repo_path: Path to the cloned repository - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run (see ``Hippocampus.run_id``) - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ + # Local bindings from review_context metadata + repo_path = review_context.metadata.repo_path + run_id = review_context.metadata.run_id + mr = review_context.metadata.mr + if not self._settings.is_signature_enabled("code_review"): logger.debug("Skipping code_review: disabled") - return [], review_context.memory if review_context else None + return [], review_context.memory # Determine which categories are active categories: list[IssueCategory] = [] @@ -214,12 +213,11 @@ async def aforward( changed_scopes = [s for s in scopes if s.has_changes and s.changed_files] if not changed_scopes: logger.info("No scopes with changes for code review") - return [], review_context.memory if review_context else None + return [], review_context.memory all_issues: list[Issue] = [] scope_memories: list[ContextMemory] = [] max_iters = self._settings.get_max_iters("code_review") - max_iters = self._settings.get_max_iters("code_review") total_files = sum(len(s.changed_files) for s in changed_scopes) logger.info( @@ -247,7 +245,7 @@ async def aforward( question = ( f"review code change of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" - ) if review_context else None + ) topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( agent, @@ -256,7 +254,7 @@ async def aforward( question=question, task_name="code_review", run_id=run_id, - initial_memory=review_context.memory if review_context else None, + initial_memory=review_context.memory, topic_ids=topic_ids, ) result = await mem.aforward( @@ -299,29 +297,23 @@ async def aforward( merged_memory = ( ContextMemory.merge(*scope_memories) if scope_memories - else (review_context.memory if review_context else None) + else review_context.memory ) return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for code issues (sync wrapper). Args: scopes: List of identified scopes with their changed files - repo_path: Path to the cloned repository - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) + return asyncio.run(self.aforward(scopes, review_context)) diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 273eee1..5f8f6ff 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -116,31 +116,30 @@ def _build_patches(self, scope: ScopeResult) -> str: async def aforward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for documentation issues. Args: scopes: List of identified scopes with their changed files - repo_path: Path to the cloned repository - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run (see ``Hippocampus.run_id``) - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ + # Local bindings from review_context metadata + repo_path = review_context.metadata.repo_path + run_id = review_context.metadata.run_id + mr = review_context.metadata.mr + if not self._settings.is_signature_enabled("doc"): logger.debug("Skipping doc: disabled") - return [], review_context.memory if review_context else None + return [], review_context.memory changed_scopes = [s for s in scopes if s.has_changes and s.changed_files] if not changed_scopes: logger.info("No scopes with changes for doc review") - return [], review_context.memory if review_context else None + return [], review_context.memory all_issues: list[Issue] = [] scope_memories: list[ContextMemory] = [] total_files = sum(len(s.changed_files) for s in changed_scopes) @@ -185,7 +184,7 @@ async def aforward( question = ( f"review documentation of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" - ) if review_context else None + ) topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( reviewer, @@ -194,7 +193,7 @@ async def aforward( question=question, task_name="doc", run_id=run_id, - initial_memory=review_context.memory if review_context else None, + initial_memory=review_context.memory, topic_ids=topic_ids, ) result = await mem.aforward( @@ -238,29 +237,23 @@ async def aforward( merged_memory = ( ContextMemory.merge(*scope_memories) if scope_memories - else (review_context.memory if review_context else None) + else review_context.memory ) return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for documentation issues (sync wrapper). Args: scopes: List of identified scopes with their changed files - repo_path: Path to the cloned repository - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) + return asyncio.run(self.aforward(scopes, review_context)) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 21c4f1e..26d81bf 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -906,20 +906,14 @@ async def _refine_scopes( self, scopes: list[ScopeResult], orphans: list[ChangedFile], - mr: MergeRequest, - repo_path: Path, - review_context: ReviewContext | None, - run_id: str | None, + review_context: ReviewContext, ) -> tuple[list[ScopeResult], "ContextMemory | None"]: """Use ReAct agent to refine scope assignments from deterministic candidates. Args: scopes: Already-resolved scope results orphans: Orphan files that couldn't be assigned - mr: The merge request - repo_path: Path to the repository root - review_context: Optional review context with memory - run_id: Pipeline run identifier + review_context: Review context with memory and metadata Returns: Tuple of (list of ScopeResult with agent-resolved assignments, @@ -929,6 +923,11 @@ async def _refine_scopes( ContextMemory, Topic, compute_common_ancestor_topic_id, make_topic_id, ) + # Local bindings from review_context metadata + mr = review_context.metadata.mr + repo_path = review_context.metadata.repo_path + run_id = review_context.metadata.run_id + # Build candidates string from already-resolved scopes candidates_str = "\n".join(self._format_candidate(s) for s in scopes) @@ -946,7 +945,7 @@ async def _refine_scopes( mem: Hippocampus | None = None async with SignatureContext("scope", self._cost_tracker): - if self._settings.get_memory_enabled("scope") and review_context: + if self._settings.get_memory_enabled("scope"): question = ( f"refine scopes of {review_context.pr_context.repo_slug}: " f"PR #{review_context.pr_context.mr_number} " @@ -960,7 +959,7 @@ async def _refine_scopes( question=question, task_name="scope", run_id=run_id, - initial_memory=review_context.memory if review_context else None, + initial_memory=review_context.memory, ) result = await mem.aforward( candidates=candidates_str, @@ -1096,28 +1095,26 @@ async def _refine_scopes( async def aforward( self, - mr: MergeRequest, - repo_path: Path, - is_local: bool = False, - run_id: str | None = None, - review_context: ReviewContext | None = None, + review_context: ReviewContext, ) -> tuple[list[ScopeResult], "ContextMemory | None"]: """Resolve scopes in the repository for the given MR. Args: - mr: The merge request to analyze - repo_path: Path to the repository root - is_local: If True, repo is already on disk - run_id: Pipeline run identifier - review_context: Review context with inherited memory + review_context: Review context with inherited memory and metadata Returns: Tuple of (list of ScopeResult, final context memory or None) """ + # Local bindings from review_context metadata + mr = review_context.metadata.mr + repo_path = review_context.metadata.repo_path + is_local = review_context.metadata.is_local + run_id = review_context.metadata.run_id + excluded_dirs = self._settings.excluded_directories reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] if not reviewable_files: - return [], review_context.memory if review_context else None + return [], review_context.memory repo = mr.repo_slug if not self._settings.is_signature_enabled("scope"): fallback = ScopeResult( @@ -1175,21 +1172,18 @@ async def aforward( logger.debug("No prior scope episode found at %s", common_dir) # Inject loaded memory into review_context for _refine_scopes if loaded_memory is not None: + merged_memory = ( + ContextMemory.merge(loaded_memory, review_context.memory) + if review_context.memory + else loaded_memory + ) review_context = ReviewContext( - pr_context=review_context.pr_context if review_context else PRContext( - repo_slug=mr.repo_slug, - mr_number=mr.number, - mr_title=mr.title or "", - summary=mr.title or "", - ), - memory=( - ContextMemory.merge(loaded_memory, review_context.memory) - if review_context and review_context.memory - else loaded_memory - ), + pr_context=review_context.pr_context, + memory=merged_memory, + metadata=review_context.metadata, ) scopes, context_memory = await self._refine_scopes( - scopes, orphans, mr, repo_path, review_context, run_id + scopes, orphans, review_context ) # Log final scopes for visibility scope_summary = "\n".join( @@ -1212,15 +1206,7 @@ async def aforward( def forward( self, - mr: MergeRequest, - repo_path: Path, - is_local: bool = False, - run_id: str | None = None, - review_context: ReviewContext | None = None, + review_context: ReviewContext, ) -> tuple[list[ScopeResult], ContextMemory | None]: """Resolve scopes (sync wrapper).""" - return asyncio.run( - self.aforward( - mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_context - ) - ) + return asyncio.run(self.aforward(review_context)) diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 442b3d0..f35f8cc 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -239,10 +239,7 @@ async def _create_osv_tools(self) -> tuple[list[Any], list[Any]]: async def aforward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for supply chain security vulnerabilities and return issues. @@ -253,24 +250,26 @@ async def aforward( Args: scopes: The scopes containing changed files to analyze - repo_path: Path to the cloned repository for reading manifest files - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run (see ``Hippocampus.run_id``) - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ + # Local bindings from review_context metadata + repo_path = review_context.metadata.repo_path + run_id = review_context.metadata.run_id + mr = review_context.metadata.mr + # Check if supply chain signature is enabled if not self._settings.is_signature_enabled("supply_chain"): logger.debug("Skipping supply_chain: disabled") - return [], review_context.memory if review_context else None + return [], review_context.memory # Check if any scope has supply-chain-relevant changes if not self._needs_analysis(scopes): logger.info("Skipping supply chain analysis: no dependency changes or Dockerfiles modified") - return [], review_context.memory if review_context else None + return [], review_context.memory all_issues: list[Issue] = [] scope_memories: list[ContextMemory] = [] @@ -332,7 +331,7 @@ async def aforward( question = ( f"review supply chain of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" - ) if review_context else None + ) topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] mem = Hippocampus( supply_chain_agent, @@ -343,7 +342,7 @@ async def aforward( question=question, task_name="supply_chain", run_id=run_id, - initial_memory=review_context.memory if review_context else None, + initial_memory=review_context.memory, topic_ids=topic_ids, ) result = await mem.aforward( @@ -361,9 +360,9 @@ async def aforward( scope.scope_path(), artifacts={"review": issues_to_markdown(issues)}, ) - # Collect scope's context memory - if mem: - scope_memories.append(mem.cmem.model_copy(deep=True)) + # Collect scope's context memory + if mem: + scope_memories.append(mem.cmem.model_copy(deep=True)) else: result = await supply_chain_agent.acall( manifest_path=manifest_path, @@ -391,29 +390,23 @@ async def aforward( merged_memory = ( ContextMemory.merge(*scope_memories) if scope_memories - else (review_context.memory if review_context else None) + else review_context.memory ) return all_issues, merged_memory def forward( self, scopes: Sequence[ScopeResult], - repo_path: Path, - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], ContextMemory | None]: """Analyze scopes for supply chain security vulnerabilities (sync wrapper). Args: scopes: The scopes containing changed files to analyze - repo_path: Path to the cloned repository for reading manifest files - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run - review_context: ReviewContext containing PR identity and inherited memory - mr: Optional merge request for topic ID computation + review_context: ReviewContext containing PR identity, inherited memory, + and runtime pipeline metadata (repo_path, run_id, mr) Returns: Tuple of (list of issues, merged context memory or None) """ - return asyncio.run(self.aforward(scopes, repo_path, run_id=run_id, review_context=review_context, mr=mr)) + return asyncio.run(self.aforward(scopes, review_context)) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index ed1d72a..447abe8 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -19,6 +19,7 @@ Issue, PRContext, ReviewContext, + ReviewMetadata, SignatureStatsResult, ReviewResult, ReviewConfig, @@ -99,11 +100,8 @@ def _get_repo_path(self, mr: MergeRequest) -> Path: async def _run_review_modules( self, scopes: list, - repo_path: Path, module_names: list[str], - run_id: str | None = None, - review_context: ReviewContext | None = None, - mr: MergeRequest | None = None, + review_context: ReviewContext, ) -> tuple[list[Issue], dict[str, ContextMemory | None]]: """Run review modules concurrently in a single event loop. @@ -113,33 +111,16 @@ async def _run_review_modules( Args: scopes: Identified scopes with changed files - repo_path: Path to the cloned repository module_names: Names of modules (for error logging) - run_id: Identifier of the pipeline run, shared across all agents - invoked within the same review run review_context: ReviewContext for Hippocampus question and memory inheritance Returns: Tuple of (aggregated list of issues, dict of module_name -> context_memory) """ - # Compute all scope topic IDs for summary/auditor modules - all_scope_topic_ids: list[str] = [] - if mr: - all_scope_topic_ids = [s.topic(mr.repo_full_name).id for s in scopes] - tasks = [ - self.code_reviewer.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - review_context=review_context, mr=mr - ), - self.doc_reviewer.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - review_context=review_context, mr=mr - ), - self.supply_chain_auditor.aforward( - scopes=scopes, repo_path=repo_path, run_id=run_id, - review_context=review_context, mr=mr - ), + self.code_reviewer.aforward(scopes=scopes, review_context=review_context), + self.doc_reviewer.aforward(scopes=scopes, review_context=review_context), + self.supply_chain_auditor.aforward(scopes=scopes, review_context=review_context), ] results = await asyncio.gather(*tasks, return_exceptions=True) @@ -215,10 +196,9 @@ def forward(self, config: ReviewConfig) -> ReviewResult: mr_title=mr.title, summary=mr.title, # Use title as placeholder since summary hasn't run ) - review_ctx = ReviewContext(pr_context=pr_context, memory=None) - scopes, initial_memory = self.scope_resolver( - mr, repo_path, is_local=is_local, run_id=run_id, review_context=review_ctx - ) + metadata = ReviewMetadata(repo_path=repo_path, run_id=run_id, mr=mr, is_local=is_local) + review_ctx = ReviewContext(pr_context=pr_context, memory=None, metadata=metadata) + scopes, initial_memory = self.scope_resolver(review_context=review_ctx) for scope in scopes: logger.info(f" Scope: {scope.subroot} ({scope.scope_type.value}) - {len(scope.changed_files)} files") if scope.package_manifest: @@ -258,13 +238,13 @@ def forward(self, config: ReviewConfig) -> ReviewResult: module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") all_issues, parallel_memories = asyncio.run( - self._run_review_modules(scopes, repo_path, module_names, run_id=run_id, review_context=review_ctx, mr=mr) + self._run_review_modules(scopes, module_names, review_context=review_ctx) ) logger.info(f"Found {len(all_issues)} issues") # Merge parallel context memories for Auditor memories_to_merge = [m for m in parallel_memories.values() if m is not None] merged_memory = ContextMemory.merge(*memories_to_merge) if memories_to_merge else summarizer_memory - review_ctx = ReviewContext(pr_context=pr_context, memory=merged_memory) + review_ctx = ReviewContext(pr_context=pr_context, memory=merged_memory, metadata=metadata) # Step 4: Run Audit (inherits merged memory from parallel modules) scoped_files = self._collect_scoped_files(scopes) logger.info( @@ -385,4 +365,4 @@ def _expand_sparse_for_scopes( # Re-checkout to materialize newly included paths repo = Repo(repo_path) - repo.git.checkout() \ No newline at end of file + repo.git.checkout() From 3cc7f4865a8860683c08957e3fc03da9f827d600 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 21:45:58 +0200 Subject: [PATCH 71/79] wip --- Dockerfile | 33 +- docs/architecture.md | 18 + poetry.lock | 4606 ++++++++--------- pyproject.toml | 4 +- src/codespy/agents/context_safe.py | 159 + .../hippocampus/modules/cartographer.py | 3 +- .../memory/hippocampus/modules/distiller.py | 3 +- .../agents/reviewer/modules/auditor.py | 109 +- .../agents/reviewer/modules/code_reviewer.py | 12 +- .../agents/reviewer/modules/doc_reviewer.py | 3 +- .../agents/reviewer/modules/scope_resolver.py | 12 +- .../agents/reviewer/modules/summarizer.py | 3 +- .../reviewer/modules/supply_chain_auditor.py | 12 +- src/codespy/agents/reviewer/reviewer.py | 25 +- 14 files changed, 2401 insertions(+), 2601 deletions(-) create mode 100644 src/codespy/agents/context_safe.py diff --git a/Dockerfile b/Dockerfile index ab7589b..f5ad0eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ # syntax=docker/dockerfile:1 -# Build stage with Poetry -FROM python:3.11-alpine AS builder +# Build stage +FROM python:3.11-slim AS builder WORKDIR /app -# Install build dependencies for Python packages with native extensions -RUN apk add --no-cache \ +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ git \ curl \ gcc \ - musl-dev \ libffi-dev \ - && pip install --no-cache-dir poetry + && pip install --no-cache-dir poetry \ + && rm -rf /var/lib/apt/lists/* # Copy project files COPY pyproject.toml poetry.lock* README.md ./ @@ -23,15 +23,19 @@ RUN poetry config virtualenvs.create false \ && poetry install --only main --no-interaction --no-ansi # Runtime stage -FROM python:3.11-alpine +FROM python:3.11-slim WORKDIR /app # Install runtime dependencies -RUN apk add --no-cache \ +RUN apt-get update && apt-get install -y --no-install-recommends \ git \ ripgrep \ - && adduser -D -u 1000 codespy + && rm -rf /var/lib/apt/lists/* \ + && useradd -m -u 1000 codespy + +# Copy Deno binary (glibc works natively on Debian) +COPY --from=denoland/deno:bin-2.9.5 /deno /usr/local/bin/deno # Copy installed packages from builder COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages @@ -43,19 +47,20 @@ COPY src/ ./src/ # Copy config to user's home directory COPY codespy.yaml /home/codespy/codespy.yaml -# Set up cache directory and DSPy local_cache directory -RUN mkdir -p /home/codespy/.cache/codespy && \ - chown -R codespy:codespy /home/codespy/.cache /home/codespy/codespy.yaml +# Pre-cache Deno/Pyodide dependencies and set up directories +ENV DENO_DIR=/home/codespy/.cache/deno +RUN mkdir -p /home/codespy/.cache/codespy \ + && (deno cache /usr/local/lib/python3.11/site-packages/dspy/primitives/runner.js || true) \ + && chown -R codespy:codespy /home/codespy/.cache /home/codespy/codespy.yaml # Switch to non-root user USER codespy -# Change to writable directory for DSPy's local_cache WORKDIR /home/codespy -# Set environment variables ENV PYTHONUNBUFFERED=1 ENV HOME=/home/codespy +ENV DENO_DIR=/home/codespy/.cache/deno ENTRYPOINT ["codespy"] CMD ["--help"] diff --git a/docs/architecture.md b/docs/architecture.md index a56eb44..5a5a919 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -88,6 +88,24 @@ CodeSpy's review pipeline follows a 4-step flow: See [Configuration](configuration.md) for per-signature settings. +### Context Window Overflow Resilience + +All signature modules are wrapped in `ContextSafe`, which provides transparent +fallback to `dspy.RLM` (Recursive Language Model) when the input exceeds the +model's context window. + +- **Pre-flight**: For models in litellm's database, estimates input tokens + before calling the module. If overflow is predicted, skips directly to RLM. +- **Try/catch**: For models not in litellm's database, catches the provider + error and retries with RLM automatically. + +RLM puts inputs into a sandboxed Python interpreter rather than the LLM prompt. +The model writes code to access and process variables (e.g., chunking large +patches via `llm_query()`), eliminating context window limits entirely. + +The composition order is: `Hippocampus(ContextSafe(Module))` — Hippocampus +handles memory, ContextSafe handles overflow, each with single responsibility. + ## Hippocampus Memory Episode-based memory that wraps DSPy agents with persistent context across reviews. Agents accumulate knowledge about a codebase scope over time — patterns, constants, parsing schemas, and reuse it in subsequent reviews of the same code area. diff --git a/poetry.lock b/poetry.lock index 2dde032..ac8ee11 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,142 +2,141 @@ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.1" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, ] [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.14.3" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, - {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, - {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, - {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, - {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, - {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, - {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, - {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, - {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, - {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, - {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, - {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, - {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, - {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, ] [package.dependencies] @@ -147,6 +146,7 @@ attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} yarl = ">=1.17.0,<2.0" [package.extras] @@ -168,44 +168,36 @@ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] -name = "alembic" -version = "1.18.3" -description = "A database migration tool for SQLAlchemy." +name = "annotated-doc" +version = "0.0.5" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." optional = false -python-versions = ">=3.10" +python-versions = ">=3.9" files = [ - {file = "alembic-1.18.3-py3-none-any.whl", hash = "sha256:12a0359bfc068a4ecbb9b3b02cf77856033abfdb59e4a5aca08b7eacd7b74ddd"}, - {file = "alembic-1.18.3.tar.gz", hash = "sha256:1212aa3778626f2b0f0aa6dd4e99a5f99b94bd25a0c1ac0bba3be65e081e50b0"}, + {file = "annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101"}, + {file = "annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb"}, ] -[package.dependencies] -Mako = "*" -SQLAlchemy = ">=1.4.23" -typing-extensions = ">=4.12" - -[package.extras] -tz = ["tzdata"] - [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] name = "anyio" -version = "4.12.1" +version = "4.14.2" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, - {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, ] [package.dependencies] @@ -213,42 +205,94 @@ idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.31.0)", "trio (>=0.32.0)"] +trio = ["trio (>=0.32.0)"] [[package]] -name = "asyncer" -version = "0.0.8" -description = "Asyncer, async and await, focused on developer experience." +name = "ast-serialize" +version = "0.8.0" +description = "Python bindings for mypy AST serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "asyncer-0.0.8-py3-none-any.whl", hash = "sha256:5920d48fc99c8f8f0f1576e1882f5022885589c5fcbc46ce4224ec3e53776eeb"}, - {file = "asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c"}, + {file = "ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6"}, + {file = "ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16"}, + {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc"}, + {file = "ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5"}, + {file = "ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87"}, + {file = "ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6"}, + {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe"}, + {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51"}, + {file = "ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0"}, + {file = "ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010"}, ] -[package.dependencies] -anyio = ">=3.4.0,<5.0" - [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" description = "Screen-scraping library" optional = false python-versions = ">=3.7.0" files = [ - {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, - {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, + {file = "beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9"}, + {file = "beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7"}, ] [package.dependencies] @@ -264,41 +308,41 @@ lxml = ["lxml"] [[package]] name = "boto3" -version = "1.42.40" +version = "1.43.72" description = "The AWS SDK for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "boto3-1.42.40-py3-none-any.whl", hash = "sha256:91d776b8b68006c1aca204d384be191883c2a36443f4a90561165986dae17b74"}, - {file = "boto3-1.42.40.tar.gz", hash = "sha256:e9e08059ae1bd47de411d361e9bfaaa6f35c8f996d68025deefff2b4dda79318"}, + {file = "boto3-1.43.72-py3-none-any.whl", hash = "sha256:f1bbbad5ed8d8a8c64edb0cd092dc443c95a85623b2ac88b6f6d633717605f00"}, + {file = "boto3-1.43.72.tar.gz", hash = "sha256:6280ce03cc85e9110fd9fb7e2fbf11eae0b1177cb041a0d69aa88edc9d178cf9"}, ] [package.dependencies] -botocore = ">=1.42.40,<1.43.0" +botocore = ">=1.43.72,<1.44.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.16.0,<0.17.0" +s3transfer = ">=0.19.0,<0.20.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.42.40" +version = "1.43.72" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "botocore-1.42.40-py3-none-any.whl", hash = "sha256:b115cdfece8162cb30f387fdff2ee4693713744c97ebb4b89742e53675dc521c"}, - {file = "botocore-1.42.40.tar.gz", hash = "sha256:6cfa07cf35ad477daef4920324f6d81b8d3a10a35baeafaa5fca22fb3ad225e2"}, + {file = "botocore-1.43.72-py3-none-any.whl", hash = "sha256:de5a1bcf8d7602c6cefc15016f15dad82981e339192531f31fa9483e11feea47"}, + {file = "botocore-1.43.72.tar.gz", hash = "sha256:1b878c69081e8e9d55aa4c0d85683e7b07f0e274a5554662f9507a46641be3d2"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} +urllib3 = ">=1.25.4,<2.2.0 || >2.2.0,<3" [package.extras] -crt = ["awscrt (==0.29.2)"] +crt = ["awscrt (==0.36.0)"] [[package]] name = "brotli" @@ -411,21 +455,26 @@ files = [ [[package]] name = "brotlicffi" -version = "1.2.0.0" +version = "1.2.0.1" description = "Python CFFI bindings to the Brotli library" optional = false python-versions = ">=3.8" files = [ - {file = "brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4"}, - {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7"}, - {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990"}, - {file = "brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6"}, - {file = "brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b"}, - {file = "brotlicffi-1.2.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fa102a60e50ddbd08de86a63431a722ea216d9bc903b000bf544149cc9b823dc"}, - {file = "brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d3c4332fc808a94e8c1035950a10d04b681b03ab585ce897ae2a360d479037c"}, - {file = "brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb4eb5830026b79a93bf503ad32b2c5257315e9ffc49e76b2715cffd07c8e3db"}, - {file = "brotlicffi-1.2.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3832c66e00d6d82087f20a972b2fc03e21cd99ef22705225a6f8f418a9158ecc"}, - {file = "brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec"}, + {file = "brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187"}, + {file = "brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4"}, + {file = "brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1"}, + {file = "brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c"}, ] [package.dependencies] @@ -436,117 +485,133 @@ cffi = [ [[package]] name = "cachetools" -version = "7.0.0" +version = "7.1.7" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.10" files = [ - {file = "cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2"}, - {file = "cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08"}, + {file = "cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0"}, + {file = "cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50"}, ] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be"}, + {file = "cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9"}, + {file = "cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659"}, + {file = "cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9"}, + {file = "cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41"}, + {file = "cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12"}, + {file = "cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af"}, + {file = "cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a"}, + {file = "cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa"}, + {file = "cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3"}, + {file = "cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0"}, + {file = "cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0"}, + {file = "cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e"}, + {file = "cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517"}, + {file = "cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735"}, + {file = "cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e"}, + {file = "cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a"}, + {file = "cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e"}, + {file = "cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6"}, + {file = "cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3"}, + {file = "cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b"}, + {file = "cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7"}, + {file = "cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac"}, + {file = "cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d"}, + {file = "cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c"}, + {file = "cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54"}, + {file = "cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03"}, + {file = "cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527"}, + {file = "cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13"}, + {file = "cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c"}, + {file = "cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48"}, + {file = "cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3"}, + {file = "cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29"}, + {file = "cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e"}, + {file = "cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f"}, + {file = "cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4"}, + {file = "cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e"}, + {file = "cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d"}, + {file = "cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4"}, + {file = "cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779"}, + {file = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688"}, + {file = "cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7"}, + {file = "cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac"}, + {file = "cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960"}, + {file = "cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc"}, + {file = "cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231"}, + {file = "cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94"}, + {file = "cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5"}, + {file = "cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66"}, + {file = "cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3"}, + {file = "cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692"}, + {file = "cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be"}, ] [package.dependencies] @@ -554,135 +619,194 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.5.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d"}, + {file = "charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9"}, + {file = "charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2"}, + {file = "charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6"}, + {file = "charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b"}, + {file = "charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00"}, + {file = "charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712"}, + {file = "charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb"}, + {file = "charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959"}, + {file = "charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win32.whl", hash = "sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d"}, + {file = "charset_normalizer-3.5.1-cp39-cp39-win_arm64.whl", hash = "sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8"}, + {file = "charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6"}, + {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [package.dependencies] @@ -710,103 +834,76 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "colorlog" -version = "6.10.1" -description = "Add colours to the output of Python's logging module." -optional = false -python-versions = ">=3.6" -files = [ - {file = "colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c"}, - {file = "colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -development = ["black", "flake8", "mypy", "pytest", "types-colorama"] - [[package]] name = "cryptography" -version = "46.0.4" +version = "50.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" -files = [ - {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32"}, - {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0"}, - {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0"}, - {file = "cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5"}, - {file = "cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b"}, - {file = "cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e"}, - {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82"}, - {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c"}, - {file = "cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061"}, - {file = "cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7"}, - {file = "cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b"}, - {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4"}, - {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b"}, - {file = "cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc"}, - {file = "cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947"}, - {file = "cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3"}, - {file = "cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.9" +files = [ + {file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"}, + {file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"}, + {file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"}, + {file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"}, + {file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"}, + {file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"}, + {file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"}, + {file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"}, + {file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"}, + {file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"}, + {file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"}, + {file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"}, + {file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"}, + {file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"}, ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "ddgs" -version = "9.10.0" +version = "9.15.0" description = "Dux Distributed Global Search. A metasearch library that aggregates results from diverse web search services." optional = false python-versions = ">=3.10" files = [ - {file = "ddgs-9.10.0-py3-none-any.whl", hash = "sha256:81233d79309836eb03e7df2a0d2697adc83c47c342713132c0ba618f1f2c6eee"}, - {file = "ddgs-9.10.0.tar.gz", hash = "sha256:d9381ff75bdf1ad6691d3d1dc2be12be190d1d32ecd24f1002c492143c52c34f"}, + {file = "ddgs-9.15.0-py3-none-any.whl", hash = "sha256:2c6cce11d8625a030ed471265230dd6d1f5e12a6efecde5dfa3a1b33882888e0"}, + {file = "ddgs-9.15.0.tar.gz", hash = "sha256:12c4148da66525031214279d3ecb5170778a484f2616cac12667d0824818d07d"}, ] [package.dependencies] @@ -814,10 +911,12 @@ click = ">=8.1.8" fake-useragent = ">=2.2.0" httpx = {version = ">=0.28.1", extras = ["brotli", "http2", "socks"]} lxml = ">=4.9.4" -primp = ">=0.15.0" +primp = ">=1.2.3" [package.extras] -dev = ["lxml-stubs", "mypy (>=1.17.1)", "pre-commit", "pytest (>=8.4.1)", "pytest-dependency (>=0.6.0)", "ruff (>=0.13.0)", "types-Pygments", "types-pexpect"] +api = ["fastapi (>=0.135.1)", "uvicorn[standard] (>=0.41.0)"] +dev = ["lxml-stubs", "mypy (>=1.17.1)", "prek", "pytest (>=8.4.1)", "pytest-trio", "ruff (>=0.13.0)", "types-PySocks", "types-PyYAML", "types-Pygments", "types-colorama", "types-decorator", "types-jsonschema", "types-pexpect", "types-psutil", "types-pyasn1", "types-ujson"] +mcp = ["mcp (>=2.0)"] [[package]] name = "diskcache" @@ -843,43 +942,42 @@ files = [ [[package]] name = "dspy" -version = "3.1.3" +version = "3.3.0" description = "DSPy" optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "dspy-3.1.3-py3-none-any.whl", hash = "sha256:26f983372ebb284324cc2162458f7bce509ef5ef7b48be4c9f490fa06ea73e37"}, - {file = "dspy-3.1.3.tar.gz", hash = "sha256:e2fd9edc8678e0abcacd5d7b901f37b84a9f48a3c50718fc7fee95a492796019"}, + {file = "dspy-3.3.0-py3-none-any.whl", hash = "sha256:358cbfb15d13246dc4a289bb2350c0ee602260c8a3869f7f63a48a9d2233e48c"}, + {file = "dspy-3.3.0.tar.gz", hash = "sha256:39aa9531391accda8acd7903b52f3c9d2efe462d4bab0c2256db5352e7392754"}, ] [package.dependencies] anyio = "*" -asyncer = "0.0.8" cachetools = ">=5.5.0" -cloudpickle = ">=3.0.0" +cloudpickle = ">=3.1.2" diskcache = ">=5.6.0" -gepa = {version = "0.0.26", extras = ["dspy"]} +gepa = {version = "0.1.1", extras = ["dspy"]} json-repair = ">=0.54.2" -litellm = ">=1.64.0" +litellm = ">=1.65.8" mcp = {version = "*", optional = true, markers = "python_version >= \"3.10\" and extra == \"mcp\""} -numpy = ">=1.26.0" -openai = ">=0.28.1" -optuna = ">=3.4.0" +openai = ">=1.66.2" orjson = ">=3.9.0" pydantic = ">=2.0" regex = ">=2023.10.3" requests = ">=2.31.0" tenacity = ">=8.2.3" tqdm = ">=4.66.1" -xxhash = ">=3.5.0" [package.extras] anthropic = ["anthropic (>=0.18.0,<1.0.0)"] -dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.64.0)", "litellm[proxy] (>=1.64.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "ruff (>=0.3.0)"] -langchain = ["langchain_core"] +dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "litellm (>=1.65.8)", "litellm[proxy] (>=1.65.8)", "numpy (>=1.26.0)", "pillow (>=10.1.0)", "pre-commit (>=3.7.0)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.26.0)", "pytest-mock (>=3.12.0)", "pytest-xdist (>=3.5.0)", "ruff (>=0.3.0)"] +langchain = ["langchain_core (>=0.3.0)"] +litellm = ["litellm (>=1.65.8)"] mcp = ["mcp"] -test-extras = ["datasets (>=2.14.6)", "langchain_core", "mcp", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] -weaviate = ["weaviate-client (>=4.5.4,<4.6.0)"] +numpy = ["numpy (>=1.26.0)"] +optuna = ["optuna (>=3.4.0)"] +test-extras = ["datasets (>=2.14.6)", "langchain_core (>=0.3.0)", "mcp", "numpy (>=1.26.0)", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] +weaviate = ["weaviate-client (>=4.5.4,<4.22.0)"] [[package]] name = "fake-useragent" @@ -981,13 +1079,13 @@ files = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.32.3" description = "A platform independent file lock." optional = false python-versions = ">=3.10" files = [ - {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, - {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, + {file = "filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09"}, + {file = "filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f"}, ] [[package]] @@ -1131,13 +1229,13 @@ files = [ [[package]] name = "fsspec" -version = "2026.1.0" +version = "2026.7.0" description = "File-system specification" optional = false python-versions = ">=3.10" files = [ - {file = "fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc"}, - {file = "fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b"}, + {file = "fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279"}, + {file = "fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88"}, ] [package.extras] @@ -1148,41 +1246,42 @@ dask = ["dask", "distributed"] dev = ["pre-commit", "ruff (>=0.5)"] doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs (>2024.2.0)", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs (>2024.2.0)", "smbprotocol", "tqdm"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs (>=2026.4.0)", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs (>=2026.6.0)", "smbprotocol", "tqdm"] fuse = ["fusepy"] -gcs = ["gcsfs"] +gcs = ["gcsfs (>=2026.4.0)"] git = ["pygit2"] github = ["requests"] -gs = ["gcsfs"] +gs = ["gcsfs (>=2026.4.0)"] gui = ["panel"] hdfs = ["pyarrow (>=1)"] http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] libarchive = ["libarchive-c"] oci = ["ocifs"] -s3 = ["s3fs"] +s3 = ["s3fs (>=2026.6.0)"] sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr"] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs (>=2026.4.0)", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "s3fs (>=2026.6.0)", "smbprotocol", "tqdm", "urllib3", "zarr (<3.2.0)", "zstandard"] tqdm = ["tqdm"] [[package]] name = "gepa" -version = "0.0.26" +version = "0.1.1" description = "A framework for optimizing textual system components (AI prompts, code snippets, etc.) using LLM-based reflection and Pareto-efficient evolutionary search." optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "gepa-0.0.26-py3-none-any.whl", hash = "sha256:331e40d8693a4192de2eb3b2b4df10d410ead49173f748d50c32a035cf746e63"}, - {file = "gepa-0.0.26.tar.gz", hash = "sha256:0119ca8022e93b6236bc154a57bb910bdb117485dc067d77777933dd3e9e9ad8"}, + {file = "gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466"}, + {file = "gepa-0.1.1.tar.gz", hash = "sha256:643fda01c23de4c9f01306e01305dd69facc29bcb34ad59e4cd07e6621d34aa1"}, ] [package.extras] build = ["build", "packaging", "requests", "semver", "setuptools (>=77.0.1)", "twine", "wheel"] dev = ["build (>=1.0.3)", "gepa[build]", "gepa[test]", "pre-commit", "ruff (>=0.3.0)"] -full = ["datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] +full = ["cloudpickle (>=3.0.0)", "datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.64.0)", "litellm (>=1.81.0)", "mlflow (>=3.0.0)", "mlflow-skinny (>=3.8.1)", "pyarrow (>=22.0.0)", "pydantic (>=2.12.0)", "tiktoken (>=0.12.0)", "tqdm (>=4.66.1)", "wandb", "wandb (>=0.23.0)"] +gskill = ["docker", "gepa[full]", "python-dotenv", "pyyaml", "swesmith"] test = ["gepa[full]", "pyright", "pytest"] [[package]] @@ -1201,87 +1300,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.46" +version = "3.1.59" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058"}, - {file = "gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f"}, + {file = "gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c"}, + {file = "gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy (==1.18.2)", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] - -[[package]] -name = "greenlet" -version = "3.3.1" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.10" -files = [ - {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5"}, - {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729"}, - {file = "greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4"}, - {file = "greenlet-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:7932f5f57609b6a3b82cc11877709aa7a98e3308983ed93552a1c377069b20c8"}, - {file = "greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f"}, - {file = "greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9"}, - {file = "greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f"}, - {file = "greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b"}, - {file = "greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4"}, - {file = "greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca"}, - {file = "greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1"}, - {file = "greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149"}, - {file = "greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a"}, - {file = "greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1"}, - {file = "greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e"}, - {file = "greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951"}, - {file = "greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2"}, - {file = "greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946"}, - {file = "greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d"}, - {file = "greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d"}, - {file = "greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683"}, - {file = "greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1"}, - {file = "greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a"}, - {file = "greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79"}, - {file = "greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab"}, - {file = "greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53"}, - {file = "greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249"}, - {file = "greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451"}, - {file = "greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil", "setuptools"] +doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] +test = ["basedpyright (==1.39.9)", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy (==1.18.2)", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] [[package]] name = "h11" @@ -1296,48 +1329,43 @@ files = [ [[package]] name = "h2" -version = "4.3.0" +version = "4.4.1" description = "Pure-Python HTTP/2 protocol implementation" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, - {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, + {file = "h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6"}, + {file = "h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516"}, ] [package.dependencies] -hpack = ">=4.1,<5" +hpack = ">=4.2,<5" hyperframe = ">=6.1,<7" [[package]] name = "hf-xet" -version = "1.2.0" +version = "1.6.0" description = "Fast transfer of large files with the Hugging Face Hub." optional = false python-versions = ">=3.8" files = [ - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649"}, - {file = "hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc"}, - {file = "hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f"}, - {file = "hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832"}, - {file = "hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e"}, - {file = "hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0"}, - {file = "hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a"}, - {file = "hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f"}, - {file = "hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848"}, - {file = "hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd"}, - {file = "hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737"}, - {file = "hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865"}, - {file = "hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69"}, - {file = "hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f"}, + {file = "hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d"}, + {file = "hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675"}, + {file = "hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b"}, + {file = "hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522"}, + {file = "hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e"}, + {file = "hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9"}, + {file = "hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338"}, + {file = "hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765"}, + {file = "hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d"}, + {file = "hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a"}, + {file = "hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f"}, + {file = "hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7"}, + {file = "hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb"}, + {file = "hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c"}, + {file = "hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b"}, + {file = "hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3"}, + {file = "hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef"}, ] [package.extras] @@ -1345,13 +1373,13 @@ tests = ["pytest"] [[package]] name = "hpack" -version = "4.1.0" +version = "4.2.0" description = "Pure-Python HPACK header encoding" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, - {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, + {file = "hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986"}, + {file = "hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0"}, ] [[package]] @@ -1416,36 +1444,36 @@ files = [ [[package]] name = "huggingface-hub" -version = "1.3.7" +version = "1.27.0" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false -python-versions = ">=3.9.0" +python-versions = ">=3.10.0" files = [ - {file = "huggingface_hub-1.3.7-py3-none-any.whl", hash = "sha256:8155ce937038fa3d0cb4347d752708079bc85e6d9eb441afb44c84bcf48620d2"}, - {file = "huggingface_hub-1.3.7.tar.gz", hash = "sha256:5f86cd48f27131cdbf2882699cbdf7a67dd4cbe89a81edfdc31211f42e4a5fd1"}, + {file = "huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d"}, + {file = "huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df"}, ] [package.dependencies] -filelock = "*" +click = ">=8.4.2,<9.0.0" +filelock = ">=3.10.0" fsspec = ">=2023.5.0" -hf-xet = {version = ">=1.2.0,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} +hf-xet = {version = ">=1.5.2,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} httpx = ">=0.23.0,<1" packaging = ">=20.9" pyyaml = ">=5.1" -shellingham = "*" tqdm = ">=4.42.1" -typer-slim = "*" typing-extensions = ">=4.1.0" [package.extras] -all = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -dev = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +all = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (>=16.2)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +dev = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (>=16.2)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-xet = ["hf-xet (>=1.2.0,<2.0.0)"] +gradio = ["gradio (>=5.0.0)", "requests"] +hf-xet = ["hf-xet (>=1.5.2,<2.0.0)"] mcp = ["mcp (>=1.8.0)"] oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] quality = ["libcst (>=1.4.0)", "mypy (==1.15.0)", "ruff (>=0.9.0)", "ty"] -testing = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (<16.0)", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +testing = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (>=16.2)", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] torch = ["safetensors[torch]", "torch"] typing = ["types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] @@ -1462,40 +1490,40 @@ files = [ [[package]] name = "idna" -version = "3.11" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "importlib-metadata" -version = "8.7.1" +version = "8.9.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, + {file = "importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f"}, + {file = "importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] +test = ["packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy (>=1.0.1)"] [[package]] name = "iniconfig" @@ -1527,113 +1555,116 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jiter" -version = "0.13.0" +version = "0.16.0" description = "Fast iterable JSON parser." optional = false python-versions = ">=3.9" files = [ - {file = "jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e"}, - {file = "jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae"}, - {file = "jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2"}, - {file = "jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5"}, - {file = "jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b"}, - {file = "jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894"}, - {file = "jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d"}, - {file = "jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096"}, - {file = "jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018"}, - {file = "jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411"}, - {file = "jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5"}, - {file = "jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3"}, - {file = "jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1"}, - {file = "jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654"}, - {file = "jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5"}, - {file = "jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663"}, - {file = "jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93"}, - {file = "jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08"}, - {file = "jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2"}, - {file = "jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228"}, - {file = "jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394"}, - {file = "jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92"}, - {file = "jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9"}, - {file = "jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf"}, - {file = "jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663"}, - {file = "jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa"}, - {file = "jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820"}, - {file = "jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68"}, - {file = "jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72"}, - {file = "jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc"}, - {file = "jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b"}, - {file = "jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10"}, - {file = "jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef"}, - {file = "jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6"}, - {file = "jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d"}, - {file = "jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d"}, - {file = "jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0"}, - {file = "jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad"}, - {file = "jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d"}, - {file = "jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df"}, - {file = "jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d"}, - {file = "jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6"}, - {file = "jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f"}, - {file = "jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d"}, - {file = "jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59"}, - {file = "jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe"}, - {file = "jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939"}, - {file = "jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9"}, - {file = "jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6"}, - {file = "jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8"}, - {file = "jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024"}, - {file = "jiter-0.13.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543"}, - {file = "jiter-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd"}, - {file = "jiter-0.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8"}, - {file = "jiter-0.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa"}, - {file = "jiter-0.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c"}, - {file = "jiter-0.13.0-cp39-cp39-win32.whl", hash = "sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7"}, - {file = "jiter-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434"}, - {file = "jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59"}, - {file = "jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19"}, - {file = "jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4"}, + {file = "jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c"}, + {file = "jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244"}, + {file = "jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f"}, + {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131"}, + {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b"}, + {file = "jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9"}, + {file = "jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26"}, + {file = "jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3"}, + {file = "jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03"}, + {file = "jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea"}, + {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91"}, + {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3"}, + {file = "jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7"}, + {file = "jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1"}, + {file = "jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056"}, + {file = "jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a"}, + {file = "jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9"}, + {file = "jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5"}, + {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730"}, + {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f"}, + {file = "jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274"}, + {file = "jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7"}, + {file = "jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331"}, + {file = "jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195"}, + {file = "jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053"}, + {file = "jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e"}, + {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb"}, + {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84"}, + {file = "jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e"}, + {file = "jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd"}, + {file = "jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a"}, + {file = "jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee"}, + {file = "jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29"}, + {file = "jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93"}, + {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a"}, + {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00"}, + {file = "jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe"}, + {file = "jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106"}, + {file = "jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8"}, + {file = "jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585"}, + {file = "jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af"}, + {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e"}, + {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077"}, + {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734"}, + {file = "jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf"}, + {file = "jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db"}, + {file = "jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce"}, + {file = "jiter-0.16.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805"}, + {file = "jiter-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7"}, + {file = "jiter-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9"}, + {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6"}, + {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d"}, + {file = "jiter-0.16.0-cp39-cp39-win32.whl", hash = "sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b"}, + {file = "jiter-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5"}, + {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702"}, + {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2"}, + {file = "jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c"}, ] [[package]] @@ -1695,271 +1726,338 @@ referencing = ">=0.31.0" [[package]] name = "librt" -version = "0.7.8" +version = "0.15.0" description = "Mypyc runtime library" optional = false python-versions = ">=3.9" files = [ - {file = "librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d"}, - {file = "librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b"}, - {file = "librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d"}, - {file = "librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d"}, - {file = "librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0"}, - {file = "librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85"}, - {file = "librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c"}, - {file = "librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f"}, - {file = "librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac"}, - {file = "librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8"}, - {file = "librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75"}, - {file = "librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873"}, - {file = "librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7"}, - {file = "librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c"}, - {file = "librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232"}, - {file = "librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63"}, - {file = "librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93"}, - {file = "librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850"}, - {file = "librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714"}, - {file = "librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449"}, - {file = "librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac"}, - {file = "librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708"}, - {file = "librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0"}, - {file = "librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc"}, - {file = "librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2"}, - {file = "librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6"}, - {file = "librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca"}, - {file = "librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93"}, - {file = "librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951"}, - {file = "librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34"}, - {file = "librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09"}, - {file = "librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418"}, - {file = "librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611"}, - {file = "librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea"}, - {file = "librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81"}, - {file = "librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83"}, - {file = "librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d"}, - {file = "librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44"}, - {file = "librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f"}, - {file = "librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b"}, - {file = "librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a"}, - {file = "librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca"}, - {file = "librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365"}, - {file = "librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32"}, - {file = "librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06"}, - {file = "librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6"}, - {file = "librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b"}, - {file = "librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5"}, - {file = "librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63"}, - {file = "librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94"}, - {file = "librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb"}, - {file = "librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be"}, - {file = "librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862"}, + {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, + {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, + {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, + {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, + {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, + {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, + {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, + {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, + {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, + {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, + {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, + {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, + {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, + {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, + {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, + {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, + {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, + {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, + {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, + {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, + {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, + {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, + {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, + {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, + {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, + {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, + {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, + {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, + {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, + {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, + {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, + {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, + {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, + {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, + {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, + {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, + {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, + {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, + {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, + {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, + {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, + {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, + {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, + {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, + {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, + {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, + {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, + {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, + {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, ] [[package]] name = "litellm" -version = "1.81.6" +version = "1.97.0" description = "Library to easily interface with LLM API providers" optional = false -python-versions = "<4.0,>=3.9" +python-versions = "<3.15,>=3.10" files = [ - {file = "litellm-1.81.6-py3-none-any.whl", hash = "sha256:573206ba194d49a1691370ba33f781671609ac77c35347f8a0411d852cf6341a"}, - {file = "litellm-1.81.6.tar.gz", hash = "sha256:f02b503dfb7d66d1c939f82e4db21aeec1d6e2ed1fe3f5cd02aaec3f792bc4ae"}, + {file = "litellm-1.97.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ff401dd5d66f54b9b474f0652c419fb7bf883fbf5ca64c0bc363acdc98b758b5"}, + {file = "litellm-1.97.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2983b40ed5d8b1bcbbfbc0d66fefa21b04db91b87c05dd680ae77cba74e561ef"}, + {file = "litellm-1.97.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e3a1f70d693716b4e8a8108f0a464b0e2c555e274ddbb8ef89c4c59c80be14ed"}, + {file = "litellm-1.97.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5b56dce7df44a6a9e6caf5379de2578a8cb82831ceabd3d71cc99b370a1015e7"}, + {file = "litellm-1.97.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b360ddc3162c2ed39b64d3f9957a7af70cd9c60cf71f7a4dfa355a0bf05bebc"}, + {file = "litellm-1.97.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3c4f1dd45e14127f2303769a7ae79482697823e329ae503513d014ceea4dd704"}, + {file = "litellm-1.97.0-cp310-abi3-win_amd64.whl", hash = "sha256:dce3377207234fc5c5b275a5e234ba056a5051fd178ae8e9a6aeb5d056f12095"}, + {file = "litellm-1.97.0.tar.gz", hash = "sha256:6f7ce326a2e5385ef850e0b0768d41f502ec79278860090a838511cea067b067"}, ] [package.dependencies] -aiohttp = ">=3.10" -click = "*" -fastuuid = ">=0.13.0" -httpx = ">=0.23.0" -importlib-metadata = ">=6.8.0" -jinja2 = ">=3.1.2,<4.0.0" -jsonschema = ">=4.23.0,<5.0.0" -openai = ">=2.8.0" -pydantic = ">=2.5.0,<3.0.0" -python-dotenv = ">=0.2.0" -tiktoken = ">=0.7.0" -tokenizers = "*" +aiohttp = ">=3.14.2,<4.0" +click = ">=8.0.0,<9.0" +fastuuid = ">=0.14.0,<1.0" +httpx = ">=0.28.0,<1.0" +importlib-metadata = ">=8.0.0,<9.0" +jinja2 = ">=3.1.6,<4.0" +jsonschema = ">=4.0.0,<5.0" +openai = ">=2.20.0,<3.0.0" +pydantic = ">=2.10.0,<3.0.0" +pydantic-settings = ">=2.14.1,<3.0" +python-dotenv = ">=1.0.0,<2.0" +tiktoken = ">=0.8.0,<1.0" +tokenizers = ">=0.21.0,<1.0" [package.extras] -caching = ["diskcache (>=5.6.1,<6.0.0)"] -extra-proxy = ["a2a-sdk (>=0.3.22,<0.4.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-iam (>=2.19.1,<3.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0)", "resend (>=0.8.0)"] -google = ["google-cloud-aiplatform (>=1.38.0)"] -grpc = ["grpcio (>=1.62.3,<1.68.dev0 || >1.71.0,!=1.71.1,!=1.72.0,!=1.72.1,!=1.73.0)", "grpcio (>=1.75.0)"] -mlflow = ["mlflow (>3.1.4)"] -proxy = ["PyJWT (>=2.10.1,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.40.76)", "cryptography", "fastapi (>=0.120.1)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.27)", "litellm-proxy-extras (==0.4.29)", "mcp (>=1.25.0,<2.0.0)", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.22,<0.0.23)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "soundfile (>=0.12.1,<0.13.0)", "uvicorn (>=0.31.1,<0.32.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=15.0.1,<16.0.0)"] -semantic-router = ["semantic-router (>=0.1.12)"] -utils = ["numpydoc"] +bedrock-realtime = ["aws-sdk-bedrock-runtime (>=0.7.0,<0.8.0)"] +caching = ["diskcache (>=5.6.3,<6.0)"] +cli = ["inquirerpy (>=0.3.4,<1.0)", "pyyaml (>=6.0.3,<7.0)", "requests (>=2.32.0,<3.0)", "rich (>=13.9.4,<14.0)"] +extra-proxy = ["a2a-sdk (>=1.1.0,<2.0)", "azure-identity (>=1.25.2,<2.0)", "azure-keyvault-secrets (>=4.10.0,<5.0)", "google-cloud-iam (>=2.19.1,<3.0)", "google-cloud-kms (>=2.24.2,<3.0)", "prisma (>=0.11.0,<1.0)", "redisvl (>=0.4.1,<1.0)", "resend (>=2.23.0,<3.0)"] +google = ["google-cloud-aiplatform (>=1.133.0,<2.0)"] +grpc = ["grpcio (==1.78.0)"] +mlflow = ["mlflow (>=3.11.1,<4.0)"] +proxy = ["apscheduler (>=3.11.2,<4.0)", "azure-identity (>=1.25.2,<2.0)", "azure-storage-blob (>=12.28.0,<13.0)", "backoff (>=2.2.1,<3.0)", "boto3 (>=1.43.1,<2.0)", "cryptography (>=49.0.0,<51.0)", "expression (>=5.6.0,<6.0)", "fastapi (>=0.136.3,<1.0)", "fastapi-sso (>=0.19.0,<1.0)", "granian (>=2.7.4,<3.0)", "gunicorn (>=23.0.0,<24.0)", "hiredis (>=3.0.0,<4.0)", "inquirerpy (>=0.3.4,<1.0)", "litellm-enterprise (==0.1.54)", "litellm-proxy-extras (==0.4.84)", "mcp (>=1.28.1,<2.0)", "orjson (>=3.11.6,<4.0)", "polars (>=1.38.1,<2.0)", "pyjwt (>=2.13.0,<3.0)", "pynacl (>=1.6.2,<2.0)", "pyroscope-io (>=0.8.16,<1.0)", "python-multipart (>=0.0.27,<1.0)", "pyyaml (>=6.0.3,<7.0)", "restrictedpython (>=8.1,<9.0)", "rich (>=13.9.4,<14.0)", "rq (>=2.7.0,<3.0)", "soundfile (>=0.12.1,<1.0)", "starlette (>=1.0.1,<2.0)", "uvicorn (>=0.33.0,<1.0)", "uvloop (>=0.21.0,<1.0)", "websockets (>=15.0.1,<16.0)"] +proxy-runtime = ["anthropic[vertex] (>=0.84.0,<1.0)", "azure-ai-contentsafety (>=1.0.0,<2.0)", "azure-storage-file-datalake (>=12.20.0,<13.0)", "ddtrace (>=4.8.2,<5.0)", "detect-secrets (>=1.5.0,<2.0)", "google-cloud-aiplatform (>=1.133.0,<2.0)", "google-genai (>=1.37.0,<2.0)", "grpcio (==1.78.0)", "langfuse (>=2.59.7,<3.0)", "llm-sandbox (>=0.3.39,<1.0)", "mangum (>=0.17.0,<1.0)", "opentelemetry-api (==1.28.0)", "opentelemetry-exporter-otlp (==1.28.0)", "opentelemetry-instrumentation-fastapi (==0.49b0)", "opentelemetry-sdk (==1.28.0)", "prometheus-client (>=0.20.0,<1.0)", "pypdf (>=6.12.0,<7.0)", "sentry-sdk (>=2.21.0,<3.0)"] +saml = ["python3-saml (>=1.16.0,<2.0)"] +semantic-router = ["aurelio-sdk (>=0.0.19,<1.0)", "semantic-router (>=0.1.15,<1.0)"] +stt-nvidia-riva = ["audioread (>=3.0.1)", "numpy (>=1.26.0)", "nvidia-riva-client (>=2.15.0)", "soundfile (>=0.12.1)"] +utils = ["numpydoc (>=1.8.0,<2.0)"] [[package]] name = "lxml" -version = "6.0.2" +version = "6.1.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" files = [ - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, - {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, - {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, - {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, - {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, - {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, - {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, - {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, - {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, - {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, - {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, - {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, - {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, - {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, - {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, - {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, - {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, - {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, - {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, - {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, - {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, - {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, - {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, - {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, - {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, - {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, - {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, - {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, - {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, - {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, - {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, - {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, - {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, - {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, - {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, - {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, - {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, - {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, - {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, - {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, - {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, - {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, - {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, - {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, - {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, - {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, - {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, - {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, - {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, - {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, - {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"}, + {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"}, + {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"}, + {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"}, + {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"}, + {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"}, + {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"}, + {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"}, + {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"}, + {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"}, + {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"}, + {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"}, + {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"}, + {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"}, + {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"}, + {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"}, + {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"}, + {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"}, + {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"}, + {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"}, + {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"}, + {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"}, + {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"}, + {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"}, + {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"}, + {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"}, + {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"}, ] [package.extras] @@ -1968,34 +2066,15 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -[[package]] -name = "mako" -version = "1.3.10" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59"}, - {file = "mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28"}, -] - -[package.dependencies] -MarkupSafe = ">=0.9.2" - -[package.extras] -babel = ["Babel"] -lingua = ["lingua"] -testing = ["pytest"] - [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] @@ -2008,17 +2087,17 @@ linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "markdownify" -version = "1.2.2" +version = "1.2.3" description = "Convert HTML to markdown." optional = false python-versions = "*" files = [ - {file = "markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a"}, - {file = "markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09"}, + {file = "markdownify-1.2.3-py3-none-any.whl", hash = "sha256:a189a0bedfd14009030fde5f85bb6f77c56897cb839b5c25315dd7d4e3e290ba"}, + {file = "markdownify-1.2.3.tar.gz", hash = "sha256:1a176f05522c8a2cb1dd3ab9d307dcdadbed5c26ae717855bfc42b3b6d38d937"}, ] [package.dependencies] @@ -2125,27 +2204,27 @@ files = [ [[package]] name = "mcp" -version = "1.26.0" +version = "1.29.0" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" files = [ - {file = "mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca"}, - {file = "mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66"}, + {file = "mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7"}, + {file = "mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36"}, ] [package.dependencies] anyio = ">=4.5" -httpx = ">=0.27.1" +httpx = ">=0.27.1,<1.0.0" httpx-sse = ">=0.4" jsonschema = ">=4.20.0" -pydantic = ">=2.11.0,<3.0.0" +pydantic = {version = ">=2.11.0,<3.0.0", markers = "python_version < \"3.14\""} pydantic-settings = ">=2.5.2" pyjwt = {version = ">=2.10.1", extras = ["crypto"]} python-multipart = ">=0.0.9" -pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} +pywin32 = {version = ">=310", markers = "sys_platform == \"win32\" and python_version < \"3.14\""} sse-starlette = ">=1.6.1" -starlette = ">=0.27" +starlette = {version = ">=0.27", markers = "python_version < \"3.14\""} typing-extensions = ">=4.9.0" typing-inspection = ">=0.4.1" uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} @@ -2323,56 +2402,71 @@ files = [ [[package]] name = "mypy" -version = "1.19.1" +version = "2.3.1" description = "Optional static typing for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, + {file = "mypy-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a936373fc690c43a8cd7e7e12a35148e4ec5aa7698ad7fc0a9f918bdc5be41"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d00d769056bde2f4e69c175071eba45cfb44fa1ed92bdfbfe64a93e0543b0cf0"}, + {file = "mypy-2.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2166b29228835e1f88ff411e96639e6ca3c7fdde84b62ec211f70f86b4051167"}, + {file = "mypy-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83d36c2924df7426333abe7faf4724a7e1aab0d9fd41625e81b4683034b80c13"}, + {file = "mypy-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:f12fdb70459d0060dea40b29e52163a961b156106d68d57882a6a9f648983a53"}, + {file = "mypy-2.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:e099200a1b1b1223a4951f0a90cbff1b8c91b250ba599dab1f7217a628144d90"}, + {file = "mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8"}, + {file = "mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8"}, + {file = "mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01"}, + {file = "mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1"}, + {file = "mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6"}, + {file = "mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff"}, + {file = "mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080"}, + {file = "mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355"}, + {file = "mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb"}, + {file = "mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b"}, + {file = "mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb"}, + {file = "mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226"}, + {file = "mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74"}, + {file = "mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6"}, + {file = "mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac"}, + {file = "mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3"}, + {file = "mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f"}, + {file = "mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82"}, + {file = "mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9"}, + {file = "mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d"}, + {file = "mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595"}, + {file = "mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc"}, + {file = "mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045"}, + {file = "mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0"}, + {file = "mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63"}, + {file = "mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4"}, + {file = "mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b"}, + {file = "mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561"}, + {file = "mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133"}, + {file = "mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9"}, + {file = "mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3"}, + {file = "mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523"}, + {file = "mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021"}, + {file = "mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e"}, + {file = "mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc"}, + {file = "mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4"}, + {file = "mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29"}, + {file = "mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb"}, + {file = "mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419"}, ] [package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +ast-serialize = ">=0.6.0,<1.0.0" +librt = {version = ">=0.13.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -typing_extensions = ">=4.6.0" +pathspec = ">=1.0.0" +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} [package.extras] dmypy = ["psutil (>=4.0)"] @@ -2392,96 +2486,15 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] -[[package]] -name = "numpy" -version = "2.4.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -files = [ - {file = "numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7"}, - {file = "numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73"}, - {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1"}, - {file = "numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32"}, - {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390"}, - {file = "numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413"}, - {file = "numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda"}, - {file = "numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695"}, - {file = "numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e"}, - {file = "numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27"}, - {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548"}, - {file = "numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f"}, - {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460"}, - {file = "numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba"}, - {file = "numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f"}, - {file = "numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85"}, - {file = "numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98"}, - {file = "numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef"}, - {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7"}, - {file = "numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499"}, - {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb"}, - {file = "numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7"}, - {file = "numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110"}, - {file = "numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622"}, - {file = "numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913"}, - {file = "numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab"}, - {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82"}, - {file = "numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f"}, - {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554"}, - {file = "numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257"}, - {file = "numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657"}, - {file = "numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b"}, - {file = "numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1"}, - {file = "numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74"}, - {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a"}, - {file = "numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325"}, - {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909"}, - {file = "numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a"}, - {file = "numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a"}, - {file = "numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75"}, - {file = "numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef"}, - {file = "numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d"}, - {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8"}, - {file = "numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5"}, - {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e"}, - {file = "numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a"}, - {file = "numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443"}, - {file = "numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236"}, - {file = "numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0"}, - {file = "numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0"}, - {file = "numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae"}, -] - [[package]] name = "openai" -version = "2.16.0" +version = "2.54.0" description = "The official Python library for the openai API" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b"}, - {file = "openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12"}, + {file = "openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b"}, + {file = "openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa"}, ] [package.dependencies] @@ -2492,150 +2505,116 @@ jiter = ">=0.10.0,<1" pydantic = ">=1.9.0,<3" sniffio = "*" tqdm = ">4" -typing-extensions = ">=4.11,<5" +typing-extensions = ">=4.14,<5" [package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"] +aiohttp = ["aiohttp (>=3.14.1)", "httpx-aiohttp (>=0.1.9)"] +bedrock = ["botocore (>=1.40.0,<2)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +httpx2 = ["anyio (>=4.10.0,<5)", "httpx (>=0.25.1,<1)", "httpx2 (>=2.7.0,<3)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] -[[package]] -name = "optuna" -version = "4.7.0" -description = "A hyperparameter optimization framework" -optional = false -python-versions = ">=3.9" -files = [ - {file = "optuna-4.7.0-py3-none-any.whl", hash = "sha256:e41ec84018cecc10eabf28143573b1f0bde0ba56dba8151631a590ecbebc1186"}, - {file = "optuna-4.7.0.tar.gz", hash = "sha256:d91817e2079825557bd2e97de2e8c9ae260bfc99b32712502aef8a5095b2d2c0"}, -] - -[package.dependencies] -alembic = ">=1.5.0" -colorlog = "*" -numpy = "*" -packaging = ">=20.0" -PyYAML = "*" -sqlalchemy = ">=1.4.2" -tqdm = "*" - -[package.extras] -checking = ["mypy", "mypy_boto3_s3", "ruff", "scipy-stubs", "types-PyYAML", "types-redis", "types-setuptools", "types-tqdm", "typing_extensions (>=3.10.0.0)"] -document = ["ase", "cmaes (>=0.12.0)", "fvcore", "kaleido (<0.4)", "lightgbm", "matplotlib (!=3.6.0)", "pandas", "pillow", "plotly (>=4.9.0)", "scikit-learn", "sphinx", "sphinx-copybutton", "sphinx-gallery", "sphinx-notfound-page", "sphinx_rtd_theme (>=1.2.0)", "torch", "torchvision"] -optional = ["boto3", "cmaes (>=0.12.0)", "google-cloud-storage", "greenlet", "grpcio", "matplotlib (!=3.6.0)", "pandas", "plotly (>=4.9.0)", "protobuf (>=5.28.1)", "redis", "scikit-learn (>=0.24.2)", "scipy", "torch"] -test = ["fakeredis[lua]", "greenlet", "grpcio", "kaleido (<0.4)", "moto", "protobuf (>=5.28.1)", "pytest", "pytest-xdist", "scipy (>=1.9.2)", "torch"] - [[package]] name = "orjson" -version = "3.11.7" +version = "3.12.0" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.10" files = [ - {file = "orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222"}, - {file = "orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa"}, - {file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e"}, - {file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2"}, - {file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c"}, - {file = "orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f"}, - {file = "orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de"}, - {file = "orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993"}, - {file = "orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c"}, - {file = "orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b"}, - {file = "orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960"}, - {file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8"}, - {file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504"}, - {file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e"}, - {file = "orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561"}, - {file = "orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d"}, - {file = "orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471"}, - {file = "orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d"}, - {file = "orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f"}, - {file = "orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f"}, - {file = "orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad"}, - {file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867"}, - {file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d"}, - {file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab"}, - {file = "orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2"}, - {file = "orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f"}, - {file = "orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74"}, - {file = "orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5"}, - {file = "orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733"}, - {file = "orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705"}, - {file = "orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace"}, - {file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b"}, - {file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157"}, - {file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3"}, - {file = "orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223"}, - {file = "orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3"}, - {file = "orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757"}, - {file = "orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539"}, - {file = "orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0"}, - {file = "orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e"}, - {file = "orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1"}, - {file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183"}, - {file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650"}, - {file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141"}, - {file = "orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2"}, - {file = "orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576"}, - {file = "orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1"}, - {file = "orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d"}, - {file = "orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49"}, + {file = "orjson-3.12.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:747843254519dd43b93eee3153a19e5a509334320c4d2f823ec879232db5c796"}, + {file = "orjson-3.12.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7c2ad193c8004254f34b499f3bd2c80f043d10754aff2b38f93da574f4883f98"}, + {file = "orjson-3.12.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bc7a872f03522d90e0429e6c0c5cd23084f767bedcb4c58048eec19294613344"}, + {file = "orjson-3.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18a87929f31d94a77f7dc93cf527e91f39ce7fe7813d588a4de2507efd32a387"}, + {file = "orjson-3.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9683ee9ea0659da64f36574ef675b8a86330c34c19ea75db1fb93c3ff99e0ef"}, + {file = "orjson-3.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:103b5db66aa53c1f9e88c2524be4f383e831ba7dfd5f9f5af6336a177c622f11"}, + {file = "orjson-3.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd57d79aefa3f84eec851d6de7a366795b9345cfaf17f82b4820430a7a5fa241"}, + {file = "orjson-3.12.0-cp310-cp310-win32.whl", hash = "sha256:3dbce9b6b3074b31a5d5dd322a9c4e5b16f206091ece4194c2e36952847a105e"}, + {file = "orjson-3.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bb17a06f9bd15237b3216c044209fe92597379124018cfc196fbb846cde64df"}, + {file = "orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92"}, + {file = "orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10"}, + {file = "orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8"}, + {file = "orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3"}, + {file = "orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e"}, + {file = "orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5"}, + {file = "orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998"}, + {file = "orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e"}, + {file = "orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710"}, + {file = "orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252"}, + {file = "orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868"}, + {file = "orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0"}, + {file = "orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54"}, + {file = "orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83"}, + {file = "orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7"}, + {file = "orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e"}, + {file = "orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b"}, + {file = "orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f"}, + {file = "orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873"}, + {file = "orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5"}, + {file = "orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a"}, + {file = "orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d"}, + {file = "orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900"}, + {file = "orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03"}, + {file = "orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8"}, + {file = "orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94"}, + {file = "orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806"}, + {file = "orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df"}, + {file = "orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978"}, + {file = "orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222"}, + {file = "orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1"}, + {file = "orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2"}, + {file = "orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e"}, + {file = "orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d"}, + {file = "orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647"}, + {file = "orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c"}, + {file = "orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc"}, + {file = "orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1"}, + {file = "orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a"}, + {file = "orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e"}, + {file = "orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f"}, + {file = "orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92"}, + {file = "orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed"}, + {file = "orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7"}, + {file = "orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e"}, + {file = "orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517"}, + {file = "orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38"}, + {file = "orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d"}, + {file = "orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13"}, + {file = "orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328"}, + {file = "orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c"}, + {file = "orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a"}, + {file = "orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55"}, + {file = "orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578"}, + {file = "orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc"}, + {file = "orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5"}, ] [[package]] name = "packaging" -version = "26.0" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" files = [ - {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, - {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] [package.extras] hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] -tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "pluggy" @@ -2654,20 +2633,42 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "primp" -version = "0.15.0" -description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints" +version = "1.3.1" +description = "HTTP client that can impersonate web browsers" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f"}, - {file = "primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299"}, - {file = "primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161"}, - {file = "primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080"}, - {file = "primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83"}, - {file = "primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260"}, - {file = "primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8"}, - {file = "primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32"}, - {file = "primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a"}, + {file = "primp-1.3.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27b87e6370045a0c65c0e4dfdfacbfe637387d05673ce8ddcce400263f7c27f0"}, + {file = "primp-1.3.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:27a8804eb9a3f641f379ee2b443591428cf85c898816e93d04d3e7b6f229ebcb"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862974796552a51af8e276bb19c5d5e189168ab8bad216aef7ce3726a8d3b1dd"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ceb24198994799706f4020a00173ba9c1b491aa9805b1e014d87946677bc3c5d"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3298b8afcf0a88ba6622bfc18e78aeb11afbb7d5afa4774f24acf7491f54a2d"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8d38c5a6d0a863274cbcae9678f265fcdcead3c20d12d152244e88f5f2186b"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96f831c78ddb5900873f51e294bf9bbb4bbfdac3a2f39ce4023f8c558d299332"}, + {file = "primp-1.3.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329d0c320841f65b39d80801d8bae126732b84ec1094ca17b14fda0bda1b20ff"}, + {file = "primp-1.3.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6c3c67670c38a03e9e8da45b212243d35afc8efa018317c46ecdce47f05329d1"}, + {file = "primp-1.3.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9409a31028a8c62a609d389554ad4f5339aad075130300cd443beef0336d7179"}, + {file = "primp-1.3.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:88ca36c2bd1b7c64b96ad07ca367d2d111ac8e9670549be5f232da8bf795d21e"}, + {file = "primp-1.3.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74d13800b501aa003fb05c263d38f8d61656c83a60b2951046c0fc412bc73976"}, + {file = "primp-1.3.1-cp310-abi3-win32.whl", hash = "sha256:09ada1752629fe89d7b128beeb59cb641f404af462e24177ba36aed1cf322299"}, + {file = "primp-1.3.1-cp310-abi3-win_amd64.whl", hash = "sha256:c0d1e294466cd5ec7ef173eedf8df25cbdc050138d40447a906e92b8553e7765"}, + {file = "primp-1.3.1-cp310-abi3-win_arm64.whl", hash = "sha256:43304cb41cbb46f361de49faf1cbdba57f969f628c9297239c7ed8ef0cac420f"}, + {file = "primp-1.3.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:72249a4540d0a8965f36eb9a86cd16801d1c7e8dac2f0b0fa23a0a5a03402d36"}, + {file = "primp-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db4e2eaa5707e47899eeba6026f420f9b0108a28c08d63f1826d0cab8d50f06f"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d62e7609c98b4bc99c9cecc47f16f332fb8fe1a023002176267b0043dedad0c7"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d692e912c2b25271163ba7719df0afdb733a7e7c3073c9094e9001882463543"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c08693517dc160a12c0f9e2565c5319173cef738893a303ff2fb28ecccbd84d"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d134ebfa31adc619e4e48289fe3e7eebc8310141560e6a6a04269cc94893d9ab"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48e27e7c0e015a6de495cf79c0c8d599ba5f69d091af31572bec2de020522d9c"}, + {file = "primp-1.3.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c3d682df08c1b1f37b1f66b21fd173baebcfcb52490830b12292d8fe89b2147"}, + {file = "primp-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fabaac4280df0802377d34b869949d617a0ecf22ca7fd5f9bded3f5c981031f1"}, + {file = "primp-1.3.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f510e5881e0a4c4b9e7dbc03722c316d58454388b88000a0e7bf18a4b36d601e"}, + {file = "primp-1.3.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0504de2901c97903a9c369856a4b186dc90a782d8320652c142b066e697d5a1a"}, + {file = "primp-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c3b24e302d95d327e873834b9423823b9c8af2abf5e0bbf57a03f3354cfe528"}, + {file = "primp-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:4346dcef805279028bf4a54bb87dd43d0920130e25b5790689f5c96c9ba0d9e5"}, + {file = "primp-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6c55f152a73b6d6af8ac37bdb648d8bbfd7e656f9ef40d87feb3c0d81cee930a"}, + {file = "primp-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:46a529d74583d6ceba52e15bf4c678fcf24e6d669c1ce935262d5490d1b25801"}, + {file = "primp-1.3.1.tar.gz", hash = "sha256:b04a5941bf9c876d011c5defaf5a25be093d56e7270b8da52c9788b9df2a829a"}, ] [package.extras] @@ -2675,133 +2676,132 @@ dev = ["certifi", "mypy (>=1.14.1)", "pytest (>=8.1.1)", "pytest-asyncio (>=0.25 [[package]] name = "propcache" -version = "0.4.1" +version = "0.5.2" description = "Accelerated property cache" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, - {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, - {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, - {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, - {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, - {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, - {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, - {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, - {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, - {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, - {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, - {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, - {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, - {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, - {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, - {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, - {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, - {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, - {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, - {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, - {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, - {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, - {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, - {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, - {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, - {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, - {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, ] [[package]] @@ -2817,18 +2817,18 @@ files = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" +pydantic-core = "2.46.4" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -2838,132 +2838,131 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] @@ -2971,13 +2970,13 @@ typing-extensions = ">=4.14.1" [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.15.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.10" files = [ - {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, - {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, + {file = "pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42"}, + {file = "pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117"}, ] [package.dependencies] @@ -2986,7 +2985,7 @@ python-dotenv = ">=0.21.0" typing-inspection = ">=0.4.0" [package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +aws-secrets-manager = ["boto3 (>=1.35.0)"] azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] toml = ["tomli (>=2.0.1)"] @@ -2994,13 +2993,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pygithub" -version = "2.8.1" +version = "2.9.1" description = "Use the full Github API v3" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0"}, - {file = "pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9"}, + {file = "pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9"}, + {file = "pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c"}, ] [package.dependencies] @@ -3012,13 +3011,13 @@ urllib3 = ">=1.26.0" [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, + {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, ] [package.extras] @@ -3026,13 +3025,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.11.0" +version = "2.13.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" files = [ - {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, - {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, + {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, + {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, ] [package.dependencies] @@ -3040,9 +3039,6 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pynacl" @@ -3087,13 +3083,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", " [[package]] name = "pytest" -version = "9.0.2" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" files = [ - {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, - {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] @@ -3108,21 +3104,21 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.10" files = [ - {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, - {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, ] [package.dependencies] -pytest = ">=8.2,<10" +pytest = ">=8.4,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -3141,13 +3137,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.3" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, + {file = "python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9"}, + {file = "python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35"}, ] [package.extras] @@ -3155,13 +3151,13 @@ cli = ["click (>=5.0)"] [[package]] name = "python-gitlab" -version = "8.0.0" +version = "8.5.0" description = "The python wrapper for the GitLab REST and GraphQL APIs." optional = false python-versions = ">=3.10.0" files = [ - {file = "python_gitlab-8.0.0-py3-none-any.whl", hash = "sha256:c635e6722c5710d35ddadfcf95c362b0aa8de11ab3972bc4f230ebd58a6c49ee"}, - {file = "python_gitlab-8.0.0.tar.gz", hash = "sha256:03eae5a9d105448796e6c0e192d402c266057e75790cf4f42c143dddf91313ce"}, + {file = "python_gitlab-8.5.0-py3-none-any.whl", hash = "sha256:94228973c54f09eccd30f5160eca91200adc31d6ed0c894221a3865b90f96426"}, + {file = "python_gitlab-8.5.0.tar.gz", hash = "sha256:628529ec4ce1f9a7ba2c145b2cf5e4eeca3015418e504b2e6fba70171b6b1b59"}, ] [package.dependencies] @@ -3169,48 +3165,49 @@ requests = ">=2.32.0" requests-toolbelt = ">=1.0.0" [package.extras] -autocompletion = ["argcomplete (>=1.10.0,<3)"] +autocompletion = ["argcomplete (>=1.10.0,<4)"] graphql = ["gql[httpx] (>=3.5.0,<5)"] yaml = ["PyYaml (>=6.0.1)"] [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.32" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.10" files = [ - {file = "python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155"}, - {file = "python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58"}, + {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, + {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, ] [[package]] name = "pywin32" -version = "311" -description = "Python for Window Extensions" +version = "312" +description = "Python for Windows Extensions" optional = false -python-versions = "*" +python-versions = ">=3.9" files = [ - {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, - {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, - {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, - {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, - {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, - {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, - {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, - {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, - {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, - {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, - {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, - {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, - {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, - {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, - {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, - {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, - {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, - {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, - {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, - {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, + {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, + {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, + {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, + {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, + {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, + {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, + {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, + {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, + {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, + {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, + {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, + {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, + {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, + {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, + {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, + {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, + {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, + {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, + {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, + {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, + {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, ] [[package]] @@ -3313,164 +3310,147 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.1.15" +version = "2026.7.19" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, - {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, - {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, - {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, - {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, - {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, - {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902"}, + {file = "regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e"}, + {file = "regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db"}, + {file = "regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327"}, + {file = "regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d"}, + {file = "regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965"}, + {file = "regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a"}, + {file = "regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5"}, + {file = "regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312"}, + {file = "regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2"}, + {file = "regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404"}, + {file = "regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e"}, + {file = "regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0"}, + {file = "regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4"}, + {file = "regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974"}, + {file = "regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4"}, + {file = "regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa"}, + {file = "regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac"}, + {file = "regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44"}, + {file = "regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78"}, + {file = "regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2"}, + {file = "regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547"}, + {file = "regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5"}, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-toolbelt" @@ -3488,13 +3468,13 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rich" -version = "14.3.2" +version = "15.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" files = [ - {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, - {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, ] [package.dependencies] @@ -3506,165 +3486,165 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.30.0" +version = "2026.6.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.10" +python-versions = ">=3.11" files = [ - {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, - {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, - {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, - {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, - {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, - {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, - {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, - {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, - {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, - {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, - {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, - {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, - {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, - {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, - {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, - {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, - {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, - {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, - {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, - {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, - {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, - {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, - {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, - {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, - {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, - {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, - {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, - {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, - {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, - {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, - {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, - {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, - {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, - {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, - {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, - {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, - {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, - {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, - {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, - {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, - {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, - {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, - {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, - {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, - {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, - {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, - {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, - {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"}, + {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"}, + {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"}, + {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"}, + {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"}, + {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"}, + {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"}, + {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"}, + {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"}, + {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"}, + {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"}, + {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"}, + {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"}, + {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"}, + {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"}, + {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"}, + {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"}, + {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"}, + {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"}, + {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"}, + {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"}, + {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"}, + {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"}, + {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"}, ] [[package]] name = "ruff" -version = "0.14.14" +version = "0.16.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed"}, - {file = "ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c"}, - {file = "ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974"}, - {file = "ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3"}, - {file = "ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b"}, - {file = "ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167"}, - {file = "ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd"}, - {file = "ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c"}, - {file = "ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b"}, + {file = "ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7"}, + {file = "ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081"}, + {file = "ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb"}, + {file = "ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506"}, + {file = "ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d"}, + {file = "ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a"}, + {file = "ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948"}, + {file = "ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a"}, + {file = "ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2"}, ] [[package]] name = "s3transfer" -version = "0.16.0" +version = "0.19.2" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"}, - {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"}, + {file = "s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25"}, + {file = "s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993"}, ] [package.dependencies] @@ -3697,13 +3677,13 @@ files = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.7" files = [ - {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, - {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, + {file = "smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f"}, + {file = "smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c"}, ] [[package]] @@ -3730,121 +3710,24 @@ files = [ [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.9.2" description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, - {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, + {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, + {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, ] -[[package]] -name = "sqlalchemy" -version = "2.0.46" -description = "Database Abstraction Library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047"}, - {file = "sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d"}, - {file = "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb"}, - {file = "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f"}, - {file = "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b"}, - {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908"}, - {file = "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede"}, - {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ac245604295b521de49b465bab845e3afe6916bcb2147e5929c8041b4ec0545"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e6199143d51e3e1168bedd98cc698397404a8f7508831b81b6a29b18b051069"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:716be5bcabf327b6d5d265dbdc6213a01199be587224eb991ad0d37e83d728fd"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6f827fd687fa1ba7f51699e1132129eac8db8003695513fcf13fc587e1bd47a5"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c805fa6e5d461329fa02f53f88c914d189ea771b6821083937e79550bf31fc19"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-win32.whl", hash = "sha256:3aac08f7546179889c62b53b18ebf1148b10244b3405569c93984b0388d016a7"}, - {file = "sqlalchemy-2.0.46-cp38-cp38-win_amd64.whl", hash = "sha256:0cc3117db526cad3e61074100bd2867b533e2c7dc1569e95c14089735d6fb4fe"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:90bde6c6b1827565a95fde597da001212ab436f1b2e0c2dcc7246e14db26e2a3"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b1e5f3a5f1ff4f42d5daab047428cd45a3380e51e191360a35cef71c9a7a2a"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93bb0aae40b52c57fd74ef9c6933c08c040ba98daf23ad33c3f9893494b8d3ce"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4e2cc868b7b5208aec6c960950b7bb821f82c2fe66446c92ee0a571765e91a5"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:965c62be8256d10c11f8907e7a8d3e18127a4c527a5919d85fa87fd9ecc2cfdc"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-win32.whl", hash = "sha256:9397b381dcee8a2d6b99447ae85ea2530dcac82ca494d1db877087a13e38926d"}, - {file = "sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl", hash = "sha256:4396c948d8217e83e2c202fbdcc0389cf8c93d2c1c5e60fa5c5a955eae0e64be"}, - {file = "sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e"}, - {file = "sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7"}, -] - -[package.dependencies] -greenlet = {version = ">=1", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} -typing-extensions = ">=4.6.0" - -[package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] -aioodbc = ["aioodbc", "greenlet (>=1)"] -aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (>=1)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] -mssql = ["pyodbc"] -mssql-pymssql = ["pymssql"] -mssql-pyodbc = ["pyodbc"] -mypy = ["mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0)"] -mysql-connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=8)"] -oracle-oracledb = ["oracledb (>=1.0.1)"] -postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] -postgresql-pg8000 = ["pg8000 (>=1.29.1)"] -postgresql-psycopg = ["psycopg (>=3.0.7)"] -postgresql-psycopg2binary = ["psycopg2-binary"] -postgresql-psycopg2cffi = ["psycopg2cffi"] -postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] -pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3_binary"] - [[package]] name = "sse-starlette" -version = "3.2.0" +version = "3.4.8" description = "SSE plugin for Starlette" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf"}, - {file = "sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422"}, + {file = "sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d"}, + {file = "sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c"}, ] [package.dependencies] @@ -3853,19 +3736,20 @@ starlette = ">=0.49.1" [package.extras] daphne = ["daphne (>=4.2.0)"] -examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "uvicorn (>=0.34.0)"] +examples = ["fastapi (>=0.115.12)", "pydantic (>=2)", "uvicorn (>=0.34.0)"] +examples-db = ["aiosqlite (>=0.21.0)", "sqlalchemy[asyncio] (>=2.0.41)"] granian = ["granian (>=2.3.1)"] uvicorn = ["uvicorn (>=0.34.0)"] [[package]] name = "starlette" -version = "0.52.1" +version = "1.6.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" files = [ - {file = "starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74"}, - {file = "starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933"}, + {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, + {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, ] [package.dependencies] @@ -3873,17 +3757,17 @@ anyio = ">=3.6.2,<5" typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "tenacity" -version = "9.1.2" +version = "9.1.4" description = "Retry code until it succeeds" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, - {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, + {file = "tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55"}, + {file = "tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a"}, ] [package.extras] @@ -3892,108 +3776,101 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false python-versions = ">=3.9" files = [ - {file = "tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970"}, - {file = "tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16"}, - {file = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030"}, - {file = "tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134"}, - {file = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a"}, - {file = "tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892"}, - {file = "tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1"}, - {file = "tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb"}, - {file = "tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa"}, - {file = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc"}, - {file = "tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded"}, - {file = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd"}, - {file = "tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967"}, - {file = "tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def"}, - {file = "tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8"}, - {file = "tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b"}, - {file = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37"}, - {file = "tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad"}, - {file = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5"}, - {file = "tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3"}, - {file = "tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd"}, - {file = "tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3"}, - {file = "tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160"}, - {file = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa"}, - {file = "tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be"}, - {file = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a"}, - {file = "tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3"}, - {file = "tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697"}, - {file = "tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16"}, - {file = "tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a"}, - {file = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27"}, - {file = "tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb"}, - {file = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e"}, - {file = "tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25"}, - {file = "tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f"}, - {file = "tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646"}, - {file = "tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88"}, - {file = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff"}, - {file = "tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830"}, - {file = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b"}, - {file = "tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b"}, - {file = "tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3"}, - {file = "tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365"}, - {file = "tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e"}, - {file = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63"}, - {file = "tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0"}, - {file = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a"}, - {file = "tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0"}, - {file = "tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71"}, - {file = "tiktoken-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d51d75a5bffbf26f86554d28e78bfb921eae998edc2675650fd04c7e1f0cdc1e"}, - {file = "tiktoken-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:09eb4eae62ae7e4c62364d9ec3a57c62eea707ac9a2b2c5d6bd05de6724ea179"}, - {file = "tiktoken-0.12.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:df37684ace87d10895acb44b7f447d4700349b12197a526da0d4a4149fde074c"}, - {file = "tiktoken-0.12.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4c9614597ac94bb294544345ad8cf30dac2129c05e2db8dc53e082f355857af7"}, - {file = "tiktoken-0.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:20cf97135c9a50de0b157879c3c4accbb29116bcf001283d26e073ff3b345946"}, - {file = "tiktoken-0.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:15d875454bbaa3728be39880ddd11a5a2a9e548c29418b41e8fd8a767172b5ec"}, - {file = "tiktoken-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cff3688ba3c639ebe816f8d58ffbbb0aa7433e23e08ab1cade5d175fc973fb3"}, - {file = "tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931"}, + {file = "tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4"}, + {file = "tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9"}, + {file = "tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e"}, + {file = "tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5"}, + {file = "tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d"}, + {file = "tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1"}, + {file = "tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910"}, + {file = "tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb"}, + {file = "tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26"}, + {file = "tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4"}, + {file = "tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173"}, + {file = "tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff"}, + {file = "tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed"}, + {file = "tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94"}, + {file = "tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791"}, + {file = "tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b"}, + {file = "tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7"}, + {file = "tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649"}, + {file = "tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b"}, + {file = "tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91"}, + {file = "tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41"}, + {file = "tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154"}, + {file = "tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545"}, + {file = "tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2"}, + {file = "tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf"}, + {file = "tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486"}, + {file = "tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615"}, + {file = "tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7"}, + {file = "tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67"}, + {file = "tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a"}, + {file = "tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d"}, + {file = "tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce"}, + {file = "tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2"}, + {file = "tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f"}, + {file = "tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec"}, + {file = "tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471"}, + {file = "tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd"}, + {file = "tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881"}, + {file = "tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24"}, + {file = "tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273"}, + {file = "tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51"}, + {file = "tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58"}, + {file = "tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b"}, + {file = "tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448"}, + {file = "tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a"}, + {file = "tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad"}, + {file = "tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e"}, + {file = "tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424"}, + {file = "tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07"}, + {file = "tiktoken-0.13.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:35e1ea1e0631c04f551297284a1ab7e1f65a3c55a9a48728d5e0f66b4527c04a"}, + {file = "tiktoken-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2a3b536c55802fe42f4b4644d2be4f04bf788506b48de0a0a658cb58f8bce232"}, + {file = "tiktoken-0.13.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b8ac2d6420ff05841a89ba5205c6d45f56c4f6843454f3c884b7eb1a2a8dddb2"}, + {file = "tiktoken-0.13.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:477c9a38e20d0ed248090509acf1e839ad3967a4f00b4b0f958210049f656dee"}, + {file = "tiktoken-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da86f8c96ac1c235d7a3b3eebff1eacfdbcfb8ad792706943268d4d2938fbafe"}, + {file = "tiktoken-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9b8858b29804b3a0add25ce9e62fb00f89f621dc754d75d03ca419d17e8ddf67"}, + {file = "tiktoken-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:b967dfb9d0adf9a631953b1b40717684f04478270fc51bbccdd2f838d67a2f00"}, + {file = "tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1"}, ] [package.dependencies] -regex = ">=2022.1.18" -requests = ">=2.26.0" +regex = "*" +requests = "*" [package.extras] -blobfile = ["blobfile (>=2)"] +blobfile = ["blobfile (>=3)"] [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.1" description = "" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c"}, - {file = "tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b"}, - {file = "tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67"}, - {file = "tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4"}, - {file = "tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a"}, - {file = "tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a"}, - {file = "tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5"}, - {file = "tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92"}, - {file = "tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48"}, - {file = "tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc"}, - {file = "tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4"}, - {file = "tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c"}, - {file = "tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195"}, - {file = "tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5"}, - {file = "tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012"}, - {file = "tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee"}, - {file = "tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37"}, - {file = "tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113"}, - {file = "tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917"}, + {file = "tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224"}, + {file = "tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4"}, + {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a"}, + {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e"}, + {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288"}, + {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4"}, + {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96"}, + {file = "tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948"}, + {file = "tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7"}, + {file = "tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9"}, + {file = "tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa"}, ] [package.dependencies] @@ -4006,73 +3883,77 @@ testing = ["datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff", [[package]] name = "tqdm" -version = "4.67.2" +version = "4.70.0" description = "Fast, Extensible Progress Meter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tqdm-4.67.2-py3-none-any.whl", hash = "sha256:9a12abcbbff58b6036b2167d9d3853042b9d436fe7330f06ae047867f2f8e0a7"}, - {file = "tqdm-4.67.2.tar.gz", hash = "sha256:649aac53964b2cb8dec76a14b405a4c0d13612cb8933aae547dd144eacc99653"}, + {file = "tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953"}, + {file = "tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] +discord = ["envwrap", "requests"] notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] [[package]] name = "tree-sitter" -version = "0.25.2" +version = "0.26.0" description = "Python bindings to the Tree-sitter parsing library" optional = false python-versions = ">=3.10" files = [ - {file = "tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20"}, - {file = "tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7"}, - {file = "tree_sitter-0.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44488e0e78146f87baaa009736886516779253d6d6bac3ef636ede72bc6a8234"}, - {file = "tree_sitter-0.25.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2f8e7d6b2f8489d4a9885e3adcaef4bc5ff0a275acd990f120e29c4ab3395c5"}, - {file = "tree_sitter-0.25.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b570690f87f1da424cd690e51cc56728d21d63f4abd4b326d382a30353acc7"}, - {file = "tree_sitter-0.25.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0ec41b895da717bc218a42a3a7a0bfcfe9a213d7afaa4255353901e0e21f696"}, - {file = "tree_sitter-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:7712335855b2307a21ae86efe949c76be36c6068d76df34faa27ce9ee40ff444"}, - {file = "tree_sitter-0.25.2-cp310-cp310-win_arm64.whl", hash = "sha256:a925364eb7fbb9cdce55a9868f7525a1905af512a559303bd54ef468fd88cb37"}, - {file = "tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b"}, - {file = "tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26"}, - {file = "tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266"}, - {file = "tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c"}, - {file = "tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f"}, - {file = "tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc"}, - {file = "tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5"}, - {file = "tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960"}, - {file = "tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c"}, - {file = "tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99"}, - {file = "tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9"}, - {file = "tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac"}, - {file = "tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897"}, - {file = "tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5"}, - {file = "tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd"}, - {file = "tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601"}, - {file = "tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053"}, - {file = "tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614"}, - {file = "tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae"}, - {file = "tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b"}, - {file = "tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8"}, - {file = "tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0"}, - {file = "tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87"}, - {file = "tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab"}, - {file = "tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358"}, - {file = "tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0"}, - {file = "tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721"}, - {file = "tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f"}, + {file = "tree_sitter-0.26.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff527388df14cb5009f9274faf78cc69a7393ae6acf3b04784b8acca249519c5"}, + {file = "tree_sitter-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7bcbadfa614326debef581957d5c780a9d7f66065c13deea61aa21d1dd36263f"}, + {file = "tree_sitter-0.26.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f941cea06128c1f74f8937a8e2a90c7db49cf4be6647cd9e07d92a306d91517"}, + {file = "tree_sitter-0.26.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e9e46b664887d8c1014f1fb33e09454bbdd9ec1fe29b7fd02dde7b46bc1bb81a"}, + {file = "tree_sitter-0.26.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:763627db05db34f12333081bd7422cc1c675893d373cc870b3e9249e200700e4"}, + {file = "tree_sitter-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:17a1c5cfd3a05d5c7c86bf4282b6ef8092c91dc0a98390499669c3fedb7d1814"}, + {file = "tree_sitter-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f289be0225ba2ace8e87d6c9639b2bc9ff2b5271afb7c5d39282a4a00e248682"}, + {file = "tree_sitter-0.26.0-cp310-cp310-win_arm64.whl", hash = "sha256:526a165a2cb1d1f79e247d400f0e0acd8d49a817d6f312d543513af200b1f886"}, + {file = "tree_sitter-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d6fe0e8fb4df77b5ee816228e2c4475a63d8cc1d4d3a7ffd7097b2b87fc3e95"}, + {file = "tree_sitter-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:514a9bf8993e5210e7970736aaf6020d1759b670e195ef17b1c48f586aa30736"}, + {file = "tree_sitter-0.26.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10f0d4eb94aa7242dcb7f554bcd24dd7ba1c114f00d58759ba08c7a46c8ec51a"}, + {file = "tree_sitter-0.26.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:335294ce0504fcefde5245dff596778ffaf820205b98ae0b549c72e48855f1d8"}, + {file = "tree_sitter-0.26.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9997ba61368c48ed54e715676afadf703947a1542464e39d047764fb3624b01"}, + {file = "tree_sitter-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c56581ad256c4195a21bfe449fed5d44a02fe83a4a7d6e70e6ec302c881191c7"}, + {file = "tree_sitter-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f8793fd18ad7eec276ed4b51c097b4bf2002b357259b66b0d75db1f3f41c754"}, + {file = "tree_sitter-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:dea4b4e27d49e9ec5b785d4f994da000e6726882fcc6ad05ec98478500c71aef"}, + {file = "tree_sitter-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c"}, + {file = "tree_sitter-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e"}, + {file = "tree_sitter-0.26.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95"}, + {file = "tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4"}, + {file = "tree_sitter-0.26.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280"}, + {file = "tree_sitter-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3"}, + {file = "tree_sitter-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37"}, + {file = "tree_sitter-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84"}, + {file = "tree_sitter-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed0889dbed843ce45ede9f5169c0b2dea2222f12685844a03fadb81f12705867"}, + {file = "tree_sitter-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6189c6c340c7384357711e3d92645e96bfb79f7a502f86de1ebdb23eb43f7dab"}, + {file = "tree_sitter-0.26.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ff2e0750b7daa722302838356d7b65e303829b7eb73c915df127ddba115e1d1"}, + {file = "tree_sitter-0.26.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7075ef857ef86f327dbb72d1e2574dda78db5754b3a1fca6506acd7fe5d561a7"}, + {file = "tree_sitter-0.26.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26c996c1edfee86e977bb3f5462e74fcec0d0b0db1e85a3c475875763caa03be"}, + {file = "tree_sitter-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2"}, + {file = "tree_sitter-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f"}, + {file = "tree_sitter-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564"}, + {file = "tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa"}, + {file = "tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3"}, + {file = "tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084"}, + {file = "tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c"}, + {file = "tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90"}, + {file = "tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa"}, + {file = "tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c"}, + {file = "tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52"}, + {file = "tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245"}, ] [package.extras] -docs = ["sphinx (>=8.1,<9.0)", "sphinx-book-theme"] -tests = ["tree-sitter-html (>=0.23.2)", "tree-sitter-javascript (>=0.23.1)", "tree-sitter-json (>=0.24.8)", "tree-sitter-python (>=0.23.6)", "tree-sitter-rust (>=0.23.2)"] +docs = ["sphinx (>=8.2,<9.0)", "sphinx-book-theme"] +tests = ["tree-sitter-html (==0.23.2)", "tree-sitter-javascript (==0.25.0)", "tree-sitter-json (==0.24.8)", "tree-sitter-python (==0.25.0)", "tree-sitter-rust (==0.24.2)"] [[package]] name = "tree-sitter-go" @@ -4219,19 +4100,20 @@ core = ["tree-sitter (>=0.24,<1.0)"] [[package]] name = "tree-sitter-rust" -version = "0.24.0" +version = "0.24.2" description = "Rust grammar for tree-sitter" optional = false python-versions = ">=3.9" files = [ - {file = "tree_sitter_rust-0.24.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7ea455443f5ab245afd8c5ce63a8ae38da455ef27437b459ce3618a9d4ec4f9a"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a0a1a2694117a0e86e156b28ee7def810ec94e52402069bf805be22d43e3c1a1"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3362992ea3150b0dd15577dd59caef4f2926b6e10806f2bb4f2533485acee2f"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2c1f4b87df568352a9e523600af7cb32c5748dc75275f4794d6f811ab13dfe"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:615f989241b717f14105b1bc621ff0c2200c86f1c3b36f1842d61f6605021152"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-win_amd64.whl", hash = "sha256:2e29be0292eaf1f99389b3af4281f92187612af31ba129e90f4755f762993441"}, - {file = "tree_sitter_rust-0.24.0-cp39-abi3-win_arm64.whl", hash = "sha256:7a0538eaf4063b443c6cd80a47df19249f65e27dbdf129396a9193749912d0c0"}, - {file = "tree_sitter_rust-0.24.0.tar.gz", hash = "sha256:c7185f482717bd41f24ffcd90b5ee24e7e0d6334fecce69f1579609994cd599d"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3620cfd12340efa43082d45df76349ff511893a9c361da2f8d6d51e307020a59"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:01a46622735498493f29f3e628a90de95c96a07bfbeb88996243eb986b1cee36"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e033c5a93b57c88e0a835880de39fc802909ff69f57aaff6000211c196ea5190"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d76d1208c3638b871236090759dfc13d478921320653a6c9da5336e7c58f65a"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87930163a462408c49ab62c667e74029bc26b4cc7123dd1bdc7352215786c64a"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da2b86099028fd42c6cd32878b7b16b01f8aac0f7b0e98742b7fa6bc3cf09b89"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-win_amd64.whl", hash = "sha256:4529c125d928882ddfb879fdc6bc0704913261ecc078b6fa7902559e0daf200d"}, + {file = "tree_sitter_rust-0.24.2-cp39-abi3-win_arm64.whl", hash = "sha256:66ba90f61bd54f4c4f5d30434957daf64507c16b0313df76becb37d63f70a227"}, + {file = "tree_sitter_rust-0.24.2.tar.gz", hash = "sha256:54fb02a5911e345308b405174465112479f56dc39e3f1e7744d7568595f00db9"}, ] [package.extras] @@ -4239,19 +4121,20 @@ core = ["tree-sitter (>=0.22,<1.0)"] [[package]] name = "tree-sitter-swift" -version = "0.0.1" +version = "0.7.3" description = "Swift grammar for tree-sitter" optional = false python-versions = ">=3.8" files = [ - {file = "tree_sitter_swift-0.0.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:45d8d61c3dc9b72ec721ed93e5920d24f87bd6350298bc7c4edc261066cda745"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c669e830cfeec55d326c79478e25465e1b61207fdb9d5459533fd5f735051d20"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caac02783238e9c72349bc4a8db11be258666d5ae1856ac54c46ca6086ecfc9"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d009e46d9800cf5ca3c585163d04c70660d1383f350dad93d08bf00735d2714b"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f61776dbf4bcb3fd9a852c69a3f61d08abaad4dd22272bf04272f7ed26aa6d75"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:4ca542091e411b2236c862a38e1c1b34323c909f011c122c10747e6010e13693"}, - {file = "tree_sitter_swift-0.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:ce3add5c7156e2e5329e675e7ac6a3389cb1e9ebff18c9248538e54fc738390d"}, - {file = "tree_sitter_swift-0.0.1.tar.gz", hash = "sha256:d43b0baf413ba4b049f92eadc074805cb6403655c905d3af24f12fce1c9f561b"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2531ec866c22ea52384e2786e07f3b2bb396c6446428a2df02cc74af3f7e6b6a"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee627e027d0868c552beca13dcdfa9944662b126f642464c5038ee3204e68340"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f38feeb4f7350c8b30d567a0dc08bf1eeaa67c241b6888d72a45a8b1a4aa7187"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eee02fecb60a07267edd123148c583d6ec9efc5d7fcb25e53da4e56869fd4cf3"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f30c30831f090ebe245f54ddcd280d2c5f7020ba17d6bbec1662bbfae140c467"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01c1e812289a2f7f01f63627a5d94a0b57d69332e8b52624becfe79ee8061651"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-win_amd64.whl", hash = "sha256:4b1de6122cbd82b2cea6d3a295f9f5f9297601b829061119e161da17a7ba7d17"}, + {file = "tree_sitter_swift-0.7.3-cp38-abi3-win_arm64.whl", hash = "sha256:af44acc50d16f284abb607ae0cf7f81011d5566283d6c62a045a549a9331a653"}, + {file = "tree_sitter_swift-0.7.3.tar.gz", hash = "sha256:a87f1dba3050a346ee3442aad8d727afd74555dea258e31c71c7934d8c04af9b"}, ] [package.extras] @@ -4279,73 +4162,55 @@ core = ["tree-sitter (>=0.23,<1.0)"] [[package]] name = "typer" -version = "0.21.1" +version = "0.27.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01"}, - {file = "typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d"}, + {file = "typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56"}, + {file = "typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df"}, ] [package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" +annotated-doc = ">=0.0.2" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +rich = ">=13.8.0" shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "typer-slim" -version = "0.21.1" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.9" -files = [ - {file = "typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d"}, - {file = "typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd"}, -] - -[package.dependencies] -click = ">=8.0.0" -typing-extensions = ">=3.7.4.3" - -[package.extras] -standard = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] @@ -4356,13 +4221,13 @@ zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.52.3" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" files = [ - {file = "uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"}, - {file = "uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea"}, + {file = "uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c"}, + {file = "uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58"}, ] [package.dependencies] @@ -4370,294 +4235,119 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "xxhash" -version = "3.6.0" -description = "Python binding for xxHash" -optional = false -python-versions = ">=3.7" -files = [ - {file = "xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71"}, - {file = "xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4"}, - {file = "xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b"}, - {file = "xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b"}, - {file = "xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb"}, - {file = "xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a"}, - {file = "xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e"}, - {file = "xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b"}, - {file = "xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3"}, - {file = "xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd"}, - {file = "xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef"}, - {file = "xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c"}, - {file = "xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0"}, - {file = "xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d"}, - {file = "xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae"}, - {file = "xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb"}, - {file = "xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c"}, - {file = "xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec"}, - {file = "xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89"}, - {file = "xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11"}, - {file = "xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd"}, - {file = "xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799"}, - {file = "xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392"}, - {file = "xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702"}, - {file = "xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1"}, - {file = "xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf"}, - {file = "xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033"}, - {file = "xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8"}, - {file = "xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e"}, - {file = "xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7"}, - {file = "xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11"}, - {file = "xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5"}, - {file = "xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f"}, - {file = "xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad"}, - {file = "xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4"}, - {file = "xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca"}, - {file = "xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93"}, - {file = "xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518"}, - {file = "xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f"}, - {file = "xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7dac94fad14a3d1c92affb661021e1d5cbcf3876be5f5b4d90730775ccb7ac41"}, - {file = "xxhash-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6965e0e90f1f0e6cb78da568c13d4a348eeb7f40acfd6d43690a666a459458b8"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab89a6b80f22214b43d98693c30da66af910c04f9858dd39c8e570749593d7e"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4903530e866b7a9c1eadfd3fa2fbe1b97d3aed4739a80abf506eb9318561c850"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4da8168ae52c01ac64c511d6f4a709479da8b7a4a1d7621ed51652f93747dffa"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97460eec202017f719e839a0d3551fbc0b2fcc9c6c6ffaa5af85bbd5de432788"}, - {file = "xxhash-3.6.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aae0c9df92e7fa46fbb738737324a563c727990755ec1965a6a339ea10a1df"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d50101e57aad86f4344ca9b32d091a2135a9d0a4396f19133426c88025b09f1"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9085e798c163ce310d91f8aa6b325dda3c2944c93c6ce1edb314030d4167cc65"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:a87f271a33fad0e5bf3be282be55d78df3a45ae457950deb5241998790326f87"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:9e040d3e762f84500961791fa3709ffa4784d4dcd7690afc655c095e02fff05f"}, - {file = "xxhash-3.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b0359391c3dad6de872fefb0cf5b69d55b0655c55ee78b1bb7a568979b2ce96b"}, - {file = "xxhash-3.6.0-cp38-cp38-win32.whl", hash = "sha256:e4ff728a2894e7f436b9e94c667b0f426b9c74b71f900cf37d5468c6b5da0536"}, - {file = "xxhash-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:01be0c5b500c5362871fc9cfdf58c69b3e5c4f531a82229ddb9eb1eb14138004"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc604dc06027dbeb8281aeac5899c35fcfe7c77b25212833709f0bff4ce74d2a"}, - {file = "xxhash-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:277175a73900ad43a8caeb8b99b9604f21fe8d7c842f2f9061a364a7e220ddb7"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfbc5b91397c8c2972fdac13fb3e4ed2f7f8ccac85cd2c644887557780a9b6e2"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2762bfff264c4e73c0e507274b40634ff465e025f0eaf050897e88ec8367575d"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f171a900d59d51511209f7476933c34a0c2c711078d3c80e74e0fe4f38680ec"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:780b90c313348f030b811efc37b0fa1431163cb8db8064cf88a7936b6ce5f222"}, - {file = "xxhash-3.6.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b242455eccdfcd1fa4134c431a30737d2b4f045770f8fe84356b3469d4b919"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a75ffc1bd5def584129774c158e108e5d768e10b75813f2b32650bb041066ed6"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fc1ed882d1e8df932a66e2999429ba6cc4d5172914c904ab193381fba825360"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:44e342e8cc11b4e79dae5c57f2fb6360c3c20cc57d32049af8f567f5b4bcb5f4"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c2f9ccd5c4be370939a2e17602fbc49995299203da72a3429db013d44d590e86"}, - {file = "xxhash-3.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02ea4cb627c76f48cd9fb37cf7ab22bd51e57e1b519807234b473faebe526796"}, - {file = "xxhash-3.6.0-cp39-cp39-win32.whl", hash = "sha256:6551880383f0e6971dc23e512c9ccc986147ce7bfa1cd2e4b520b876c53e9f3d"}, - {file = "xxhash-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c35c4cdc65f2a29f34425c446f2f5cdcd0e3c34158931e1cc927ece925ab802"}, - {file = "xxhash-3.6.0-cp39-cp39-win_arm64.whl", hash = "sha256:ffc578717a347baf25be8397cb10d2528802d24f94cfc005c0e44fef44b5cdd6"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd"}, - {file = "xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d"}, - {file = "xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6"}, -] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.20)", "websockets (>=13.0)"] [[package]] name = "yarl" -version = "1.22.0" +version = "1.24.5" description = "Yet another URL library" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, - {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, - {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, - {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, - {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, - {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, - {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, - {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, - {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, - {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, - {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, - {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, - {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, - {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, - {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, - {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, - {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, - {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, - {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, - {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, - {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, - {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, - {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, - {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, - {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, - {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, - {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, ] [package.dependencies] @@ -4667,24 +4357,24 @@ propcache = ">=0.2.1" [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, + {file = "zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f"}, + {file = "zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] +type = ["pytest-mypy (>=1.0.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "9a3b7297f4254b101ea7184fad127421f243734e6923014c393615f87f7f7db6" +content-hash = "a299c43b1085850017f1a21d51aac5d8e1c77a140989a432d2defd0a2e0a6855" diff --git a/pyproject.toml b/pyproject.toml index b802600..ff9e576 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.11,<3.14" -dspy = {version = "^3.1.3", extras = ["mcp"]} +dspy = {version = "^3.3.0", extras = ["mcp"]} litellm = "^1.81.6" cachetools = ">=5.0.0" PyGithub = ">=2.5.0" @@ -46,7 +46,7 @@ tree-sitter-kotlin = ">=1.0" tree-sitter-objc = ">=3.0" tree-sitter-rust = ">=0.23" tree-sitter-hcl = ">=1.2.0" -mcp = ">=1.0.0" +mcp = ">=1.29.0,<2.0.0" beautifulsoup4 = ">=4.12.0" markdownify = ">=0.13.0" ddgs = ">=8.0.0" diff --git a/src/codespy/agents/context_safe.py b/src/codespy/agents/context_safe.py new file mode 100644 index 0000000..93c0670 --- /dev/null +++ b/src/codespy/agents/context_safe.py @@ -0,0 +1,159 @@ +"""ContextSafe wrapper for context window overflow resilience. + +Provides transparent fallback to RLM (Recursive Language Model) when inputs +exceed the model's context window. Used to wrap all DSPy signature modules. +""" + +import logging +import re + +import dspy # type: ignore[import-untyped] +import litellm # type: ignore[import-untyped] + + +logger = logging.getLogger(__name__) + +# Regex for detecting context window overflow in error messages +_RE_CONTEXT_LENGTH = re.compile(r"maximum context length is \d+ tokens", re.IGNORECASE) + + +def estimate_context_overflow(model: str, max_tokens: int, input_text: str) -> bool: + """Estimate whether input + max_tokens would exceed the model's context window. + + Returns True if overflow is likely. Returns False if no overflow expected + or if the model is not in litellm's DB (can't estimate). + """ + SAFETY_MARGIN = 4096 + try: + info = litellm.get_model_info(model) + max_input = info.get("max_input_tokens") or 0 + if not max_input: + return False + estimated_input = litellm.token_counter(model=model, text=input_text) + return (estimated_input + max_tokens + SAFETY_MARGIN) > max_input + except Exception: + return False + + +def is_context_overflow_error(exc: Exception) -> bool: + """Detect context window overflow from any source. + + Handles two cases: + - Known models: dspy raises dspy.ContextWindowExceededError + - Unknown models (nvidia/zai Bedrock): litellm raises BadRequestError, + dspy wraps as LMInvalidRequestError — detected via error message regex. + """ + if isinstance(exc, dspy.ContextWindowExceededError): + return True + return bool(_RE_CONTEXT_LENGTH.search(str(exc))) + + +class ContextSafe(dspy.Module): + """Wraps a dspy.Module and falls back to RLM on context window overflow. + + Two-layer defense: + - Pre-flight: estimates input tokens via litellm; if overflow is predicted + for a known model, skips the inner module and uses RLM directly. + - Try/catch: if the inner module raises a context overflow error (unknown + models where pre-flight can't estimate), catches it and retries with RLM. + + Transparent to Hippocampus — delegates .signature to inner module. + """ + + def __init__(self, module: dspy.Module, signature, tools: list | None = None, name: str = ""): + super().__init__() + self.module = module + self._orig_signature = signature + self._tools = tools + self._name = name or signature.__name__ + + @property + def signature(self): + return getattr(self.module, "signature", None) + + @signature.setter + def signature(self, value): + self.module.signature = value + + def forward(self, **kwargs) -> dspy.Prediction: + if self._would_overflow(kwargs): + logger.warning( + "ContextSafe[%s]: pre-flight detected context overflow for model=%s; " + "falling back to RLM", + self._name, + getattr(dspy.settings.lm, "model", "unknown"), + ) + return self._create_rlm_fallback()(**kwargs) + + try: + return self.module(**kwargs) + except Exception as exc: + if not is_context_overflow_error(exc): + raise + logger.warning( + "ContextSafe[%s]: context window overflow on model=%s; " + "falling back to RLM (error: %s)", + self._name, + getattr(dspy.settings.lm, "model", "unknown"), + str(exc)[:200], + ) + return self._create_rlm_fallback()(**kwargs) + + async def aforward(self, **kwargs) -> dspy.Prediction: + """Async path — used by code_review, scope, supply_chain via Hippocampus.aforward.""" + if self._would_overflow(kwargs): + logger.warning( + "ContextSafe[%s]: pre-flight detected context overflow for model=%s; " + "falling back to RLM", + self._name, + getattr(dspy.settings.lm, "model", "unknown"), + ) + return await self._create_rlm_fallback().acall(**kwargs) + + try: + return await self.module.acall(**kwargs) + except Exception as exc: + if not is_context_overflow_error(exc): + raise + logger.warning( + "ContextSafe[%s]: context window overflow on model=%s; " + "falling back to RLM (error: %s)", + self._name, + getattr(dspy.settings.lm, "model", "unknown"), + str(exc)[:200], + ) + return await self._create_rlm_fallback().acall(**kwargs) + + def _would_overflow(self, kwargs: dict) -> bool: + """Pre-flight: estimate overflow from current LM config and input size.""" + try: + lm = dspy.settings.lm + if lm is None: + return False + model = lm.model + max_tokens = lm.kwargs.get("max_tokens") or 0 + if not max_tokens: + return False + input_text = "\n".join(str(v) for v in kwargs.values()) + return estimate_context_overflow(model, max_tokens, input_text) + except Exception: + return False + + def _get_current_signature(self): + """Get current signature (may include context_memory if Hippocampus modified it).""" + sig = getattr(self.module, "signature", None) + if sig is not None: + return sig + preds = list(self.module.named_predictors()) + if preds: + return preds[0][1].signature + return self._orig_signature + + def _create_rlm_fallback(self) -> dspy.RLM: + """Create RLM with current signature and tools.""" + return dspy.RLM( + self._get_current_signature(), + tools=self._tools, + max_iters=10, + max_llm_calls=20, + ) diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index d6828a9..8a310c2 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -2,6 +2,7 @@ import dspy +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, ItemTag, @@ -139,7 +140,7 @@ class Cartographer(dspy.Module): def __init__(self): super().__init__() - self.predict = dspy.ChainOfThought(CartographerSig) + self.predict = ContextSafe(dspy.ChainOfThought(CartographerSig), CartographerSig, name="cartographer") def forward(self, diagnosis, item_tags, cache_candidates, current_map, question, token_budget, current_tokens, max_context_item_tokens): diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index a75f1c6..96049d5 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -2,6 +2,7 @@ import dspy +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, ItemTag, @@ -151,7 +152,7 @@ class Distiller(dspy.Module): def __init__(self): super().__init__() - self.predict = dspy.ChainOfThought(DistillerSig) + self.predict = ContextSafe(dspy.ChainOfThought(DistillerSig), DistillerSig, name="distiller") def forward( self, diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 5f4f637..ab7e449 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -4,9 +4,9 @@ from typing import TYPE_CHECKING, Sequence import dspy -import litellm from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, ReviewContext from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder @@ -20,11 +20,6 @@ logger = logging.getLogger(__name__) -def _strip_patches(files: Sequence[ChangedFile]) -> list[ChangedFile]: - """Remove patch content from files to reduce token usage.""" - return [f.model_copy(update={"patch": None}) for f in files] - - class AuditSignature(dspy.Signature): """Assess code quality and provide a recommendation for a merge request. @@ -61,49 +56,6 @@ def __init__(self) -> None: self._cost_tracker = get_cost_tracker() self._settings = get_settings() - def _would_overflow_context( - self, - mr_title: str, - summary: str, - changed_files: list[ChangedFile], - all_issues: list[Issue], - context_memory: str | None = None, - ) -> bool: - """Estimate whether the input would overflow the model's context window. - - Uses litellm.token_counter for estimation with a safety margin to - account for DSPy formatting overhead (system prompt, field descriptions, - ChainOfThought instructions). - """ - SAFETY_MARGIN = 4096 # DSPy formatting overhead + token counting imprecision - - try: - llm_config = self._settings.get_llm_config("audit") - model = llm_config.model - max_tokens = llm_config.max_tokens or self._settings.default_max_tokens - - # Get model limits - info = litellm.get_model_info(model) - max_input = info.get("max_input_tokens") or 0 - max_output = info.get("max_output_tokens") or 0 - if not max_input: - return False # Unknown model, can't estimate - - # Use max_input as context window proxy (conservative). - # For shared-budget models the true window is slightly larger, - # but using max_input ensures we never overshoot. - context_window = max_input - - # Estimate input tokens from a rough serialization - input_text = f"{mr_title}\n{summary}\n{changed_files}\n{all_issues}" - if context_memory: - input_text += f"\n{context_memory}" - estimated_input = litellm.token_counter(model=model, text=input_text) - - return (estimated_input + max_tokens + SAFETY_MARGIN) > context_window - except Exception: - return False # Estimation failed; proceed with full input - def _call_auditor( self, auditor: dspy.ChainOfThought, @@ -193,57 +145,18 @@ def forward( "NEEDS_DISCUSSION" if all_issues else "APPROVE", ) - auditor = dspy.ChainOfThought(AuditSignature) + auditor = ContextSafe(dspy.ChainOfThought(AuditSignature), AuditSignature, name="audit") logger.info("Running audit...") - # Pre-flight: check if full input would overflow - context_memory_str = ( - review_context.memory.render() if review_context.memory else None - ) - if self._would_overflow_context( - mr_title=review_context.pr_context.mr_title, - summary=review_context.pr_context.summary, - changed_files=list(changed_files), - all_issues=list(all_issues), - context_memory=context_memory_str, - ): - logger.info( - "Pre-flight: stripping patches from changed_files to fit context window" - ) - audit_files = _strip_patches(changed_files) - else: - audit_files = list(changed_files) - with SignatureContext("audit", self._cost_tracker): - try: - result = self._call_auditor( - auditor, - review_context, - audit_files, - list(all_issues), - run_id, - scopes, - topic_ids, - ) - except dspy.ContextWindowExceededError: - if audit_files is not _strip_patches(changed_files): - # Pre-flight didn't strip — try again without patches - logger.warning( - "Context window exceeded despite pre-flight check; " - "retrying without patches" - ) - audit_files = _strip_patches(changed_files) - result = self._call_auditor( - auditor, - review_context, - audit_files, - list(all_issues), - run_id, - scopes, - topic_ids, - ) - else: - # Already stripped patches and still overflowing — re-raise - raise + result = self._call_auditor( + auditor, + review_context, + list(changed_files), + list(all_issues), + run_id, + scopes, + topic_ids, + ) return result.quality_assessment, result.recommendation diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 554a86b..491e153 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.tools.git.models import MergeRequest @@ -229,10 +230,15 @@ async def aforward( scope_root = resolve_scope_root(repo_path, scope.subroot) tools, contexts = await self._create_tools(scope_root) try: - agent = dspy.ReAct( - signature=CodeReviewSignature, + agent = ContextSafe( + dspy.ReAct( + signature=CodeReviewSignature, + tools=tools, + max_iters=max_iters, + ), + CodeReviewSignature, tools=tools, - max_iters=max_iters, + name="code_review", ) scoped = make_scope_relative(scope) logger.info( diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 5f8f6ff..8f03804 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.tools.git.models import MergeRequest @@ -173,7 +174,7 @@ async def aforward( logger.debug(f" No patches in {scope.subroot}, skipping doc review") continue try: - reviewer = dspy.ChainOfThought(DocReviewSignature) + reviewer = ContextSafe(dspy.ChainOfThought(DocReviewSignature), DocReviewSignature, name="doc") logger.info( f" Doc review: scope {scope.subroot} " f"({len(scope.changed_files)} files)" diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 26d81bf..1ae56ac 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, Field from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import ( PRContext, @@ -937,10 +938,15 @@ async def _refine_scopes( max_iters = self._settings.get_max_iters("scope") tools, contexts = await self._create_tools(repo_path) try: - agent = dspy.ReAct( - signature=ScopeRefinementSignature, + agent = ContextSafe( + dspy.ReAct( + signature=ScopeRefinementSignature, + tools=tools, + max_iters=max_iters, + ), + ScopeRefinementSignature, tools=tools, - max_iters=max_iters, + name="scope", ) mem: Hippocampus | None = None diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 27da842..db02157 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -6,6 +6,7 @@ import dspy from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder from codespy.config import get_settings @@ -97,7 +98,7 @@ def forward( "Merged %d prior scope episode(s) into summarizer memory", len(per_scope_memories), ) - summarizer = dspy.ChainOfThought(PRSummarySignature) + summarizer = ContextSafe(dspy.ChainOfThought(PRSummarySignature), PRSummarySignature, name="summary") logger.info("Generating PR summary...") question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index f35f8cc..43437e2 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -8,6 +8,7 @@ import dspy # type: ignore[import-untyped] from codespy.agents import SignatureContext, get_cost_tracker +from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.tools.git.models import MergeRequest @@ -314,10 +315,15 @@ async def aforward( try: # Combine scoped filesystem tools with shared OSV tools all_tools = scoped_tools + osv_tools - supply_chain_agent = dspy.ReAct( - signature=SupplyChainSecuritySignature, + supply_chain_agent = ContextSafe( + dspy.ReAct( + signature=SupplyChainSecuritySignature, + tools=all_tools, + max_iters=supply_chain_max_iters, + ), + SupplyChainSecuritySignature, tools=all_tools, - max_iters=supply_chain_max_iters, + name="supply_chain", ) logger.debug( diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 447abe8..ea537b5 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -233,7 +233,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: ) # Enrich review_ctx with actual summary and memory from summarizer pr_context.summary = pr_summary - review_ctx = ReviewContext(pr_context=pr_context, memory=summarizer_memory) + review_ctx = ReviewContext(pr_context=pr_context, memory=summarizer_memory, metadata=metadata) # Step 3: Run review modules concurrently via asyncio.gather (inherit Scope Identifier memory) module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") @@ -251,21 +251,14 @@ def forward(self, config: ReviewConfig) -> ReviewResult: f"Audit input: {len(scoped_files)} in-scope files " f"(filtered from {len(mr.changed_files)} total)" ) - try: - quality_assessment, recommendation = self.auditor( - review_context=review_ctx, - changed_files=scoped_files, - all_issues=all_issues, - run_id=run_id, - scopes=scopes, - topic_ids=all_scope_topic_ids, - ) - except dspy.ContextWindowExceededError: - logger.warning( - "Audit skipped: input exceeds model context window even without patches." - ) - quality_assessment = "Audit skipped due to context window limit." - recommendation = "NEEDS_DISCUSSION" if all_issues else "APPROVE" + quality_assessment, recommendation = self.auditor( + review_context=review_ctx, + changed_files=scoped_files, + all_issues=all_issues, + run_id=run_id, + scopes=scopes, + topic_ids=all_scope_topic_ids, + ) # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() return ReviewResult( From d9ea599be91c4e051e3656b245013acbab7cc107 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 22:28:39 +0200 Subject: [PATCH 72/79] wip --- .../memory/hippocampus/context_memory.py | 13 ++ .../agents/memory/hippocampus/hippocampus.py | 48 ++++--- tests/test_context_memory.py | 21 +++ tests/test_hippocampus.py | 135 ++++++++++++++++++ 4 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 tests/test_hippocampus.py diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 478f745..542364e 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os import uuid from enum import Enum @@ -7,6 +8,8 @@ from pydantic import BaseModel, Field +logger = logging.getLogger(__name__) + class ItemTag(str, Enum): """How a context-memory item performed in the trajectory just observed. @@ -292,12 +295,22 @@ def apply(self, ops: list[Operation], topic_ids: list[str] | None = None) -> tup lst[:] = [it for it in lst if it.id != op.item_id] elif op.type == OpType.REPLACE and op.item_id and op.content: + replaced = False for sec in cm.section_names(): lst = cm.section(sec) for i, it in enumerate(lst): if it.id == op.item_id: # Preserve existing topic_ids on REPLACE lst[i] = Item(id=it.id, content=op.content, topic_ids=it.topic_ids) + replaced = True + break + if replaced: + break + if not replaced: + logger.warning( + "REPLACE target %r not found in context memory; skipping", + op.item_id, + ) elif op.type == OpType.ADD and op.section and op.content: prefix = _SECTION_PREFIX.get(op.section, op.section[:2]) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 9ac63aa..b4faf8f 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -498,7 +498,7 @@ def _record_mutations( List of Mutation records for this step. """ mutations: list[Mutation] = [] - add_indices: list[int] = [] + add_mutations: list[Mutation] = [] for op in ops: if op.type == OpType.DELETE and op.item_id: found = pre_memory.find_item(op.item_id) @@ -531,23 +531,35 @@ def _record_mutations( ) ) elif op.type == OpType.ADD and op.section and op.content: - add_indices.append(len(mutations)) - mutations.append( - Mutation( - step=self._distill_step, - type=OpType.ADD, - item_id="", - section=op.section, - content=op.content, - previous_content=None, - topic_ids=list(self._topic_ids), - ) + mut = Mutation( + step=self._distill_step, + type=OpType.ADD, + item_id="", + section=op.section, + content=op.content, + previous_content=None, + topic_ids=list(self._topic_ids), ) + mutations.append(mut) + add_mutations.append(mut) # Back-fill ADD mutation item_ids from new_ids - for i, new_id in zip(add_indices, new_ids): - mutations[i].item_id = new_id + for mut, new_id in zip(add_mutations, new_ids, strict=True): + mut.item_id = new_id return mutations + def _update_item_scores(self, tags: dict[str, ItemTag]) -> None: + """Adjust item scores based on Distiller-assigned tags. + + HELPFUL: +1, HARMFUL/STALE: -1, NEUTRAL: ensure entry exists (default 0). + """ + for bid, tag in tags.items(): + if tag == ItemTag.HELPFUL: + self.scores[bid] = self.scores.get(bid, 0) + 1 + elif tag in (ItemTag.HARMFUL, ItemTag.STALE): + self.scores[bid] = self.scores.get(bid, 0) - 1 + else: + self.scores.setdefault(bid, 0) + def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, @@ -558,13 +570,7 @@ def _distill(self, trajectory: str, question: str) -> None: known = self.cmem.ids() tags = {k: v for k, v in (distilled.item_tags or {}).items() if k in known} - for bid, tag in tags.items(): - if tag == ItemTag.HELPFUL: - self.scores[bid] = self.scores.get(bid, 0) + 1 - elif tag in (ItemTag.HARMFUL, ItemTag.STALE): - self.scores[bid] = self.scores.get(bid, 0) - 1 - else: - self.scores.setdefault(bid, 0) + self._update_item_scores(tags) edits = self.cartograph( diagnosis=distilled.diagnosis, diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index b0a2423..608c2fa 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -366,6 +366,27 @@ def test_delete_removes_item(self): assert len(new_memory.context_understanding) == 0 + def test_replace_nonexistent_logs_warning(self, caplog): + """REPLACE on non-existent item logs warning and leaves memory unchanged.""" + import logging + + memory = ContextMemory( + context_understanding=[ + Item(id="cu-abc", content="Existing", topic_ids=["t1"]), + ], + ) + ops = [Operation(type=OpType.REPLACE, item_id="cu-GONE", content="New content")] + with caplog.at_level( + logging.WARNING, + logger="codespy.agents.memory.hippocampus.context_memory", + ): + new_memory, new_ids = memory.apply(ops, topic_ids=["t2"]) + + assert new_ids == [] + assert new_memory.context_understanding[0].content == "Existing" + assert "cu-GONE" in caplog.text + assert "not found" in caplog.text + class TestContextMemoryMerge: """Tests for ContextMemory.merge() method.""" diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py new file mode 100644 index 0000000..fad437e --- /dev/null +++ b/tests/test_hippocampus.py @@ -0,0 +1,135 @@ +"""Tests for Hippocampus internal methods.""" + +import pytest + +from codespy.agents.memory.hippocampus import ( + ContextMemory, + Hippocampus, + Item, + ItemTag, + Operation, + OpType, +) + + +class TestRecordMutations: + """Unit tests for _record_mutations back-fill logic.""" + + @staticmethod + def _make_hip(topic_ids=None, distill_step=0): + """Create minimal Hippocampus bypassing __init__ for unit testing.""" + hip = object.__new__(Hippocampus) + hip._distill_step = distill_step + hip._topic_ids = topic_ids or ["t1"] + return hip + + def test_add_backfill_with_mixed_ops(self): + """ADD item_ids back-filled correctly when DELETEs precede them.""" + pre = ContextMemory( + context_understanding=[Item(id="cu-existing", content="Old", topic_ids=["t1"])], + ) + ops = [ + Operation(type=OpType.DELETE, item_id="cu-existing"), + Operation(type=OpType.ADD, section="context_understanding", content="New 1"), + Operation(type=OpType.ADD, section="domain_constants", content="New 2"), + ] + new_ids = ["cu-aaa", "dc-bbb"] + + hip = self._make_hip() + mutations = hip._record_mutations(ops, new_ids, pre) + + assert len(mutations) == 3 + assert mutations[0].type == OpType.DELETE + assert mutations[0].item_id == "cu-existing" + assert mutations[1].type == OpType.ADD + assert mutations[1].item_id == "cu-aaa" + assert mutations[2].type == OpType.ADD + assert mutations[2].item_id == "dc-bbb" + + def test_add_backfill_skipped_delete(self): + """ADD correct even when DELETE target not found (no mutation emitted).""" + pre = ContextMemory() # empty — DELETE won't find anything + ops = [ + Operation(type=OpType.DELETE, item_id="cu-ghost"), + Operation(type=OpType.ADD, section="context_understanding", content="New"), + ] + new_ids = ["cu-xyz"] + + hip = self._make_hip() + mutations = hip._record_mutations(ops, new_ids, pre) + + assert len(mutations) == 1 + assert mutations[0].type == OpType.ADD + assert mutations[0].item_id == "cu-xyz" + + def test_add_backfill_all_adds(self): + """All-ADD batch back-fills in order.""" + pre = ContextMemory() + ops = [ + Operation(type=OpType.ADD, section="context_understanding", content="A"), + Operation(type=OpType.ADD, section="domain_constants", content="B"), + Operation(type=OpType.ADD, section="reusable_results", content="C"), + ] + new_ids = ["cu-1", "dc-2", "rr-3"] + + hip = self._make_hip() + mutations = hip._record_mutations(ops, new_ids, pre) + + assert [m.item_id for m in mutations] == ["cu-1", "dc-2", "rr-3"] + assert all(m.type == OpType.ADD for m in mutations) + + def test_add_backfill_length_mismatch_raises(self): + """zip(strict=True) raises ValueError on length mismatch.""" + pre = ContextMemory() + ops = [ + Operation(type=OpType.ADD, section="context_understanding", content="A"), + ] + new_ids = ["cu-1", "cu-2"] # too many IDs + + hip = self._make_hip() + with pytest.raises(ValueError): + hip._record_mutations(ops, new_ids, pre) + + +class TestUpdateItemScores: + """Unit tests for _update_item_scores scoring logic.""" + + @staticmethod + def _make_hip(): + """Create minimal Hippocampus with empty scores.""" + hip = object.__new__(Hippocampus) + hip.scores = {} + return hip + + def test_helpful_increments(self): + hip = self._make_hip() + hip._update_item_scores({"a": ItemTag.HELPFUL}) + assert hip.scores == {"a": 1} + + def test_helpful_accumulates(self): + hip = self._make_hip() + hip.scores = {"a": 3} + hip._update_item_scores({"a": ItemTag.HELPFUL}) + assert hip.scores == {"a": 4} + + def test_harmful_decrements(self): + hip = self._make_hip() + hip.scores = {"a": 2} + hip._update_item_scores({"a": ItemTag.HARMFUL}) + assert hip.scores == {"a": 1} + + def test_stale_decrements(self): + hip = self._make_hip() + hip._update_item_scores({"a": ItemTag.STALE}) + assert hip.scores == {"a": -1} + + def test_neutral_initializes_zero(self): + hip = self._make_hip() + hip._update_item_scores({"a": ItemTag.NEUTRAL}) + assert hip.scores == {"a": 0} + + def test_neutral_preserves_existing(self): + hip = self._make_hip() + hip.scores = {"a": 5} + hip._update_item_scores({"a": ItemTag.NEUTRAL}) + assert hip.scores == {"a": 5} From 73a7fbbf38712b0ff1f1e3d4645ec8a1df766865 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 23:15:31 +0200 Subject: [PATCH 73/79] wip --- docs/usage.md | 2 +- src/codespy/agents/reviewer/models.py | 22 ++--- .../agents/reviewer/modules/auditor.py | 14 ++-- .../agents/reviewer/modules/code_reviewer.py | 8 +- .../agents/reviewer/modules/doc_reviewer.py | 8 +- .../agents/reviewer/modules/scope_resolver.py | 76 +++++++++--------- .../agents/reviewer/modules/summarizer.py | 36 ++++----- .../reviewer/modules/supply_chain_auditor.py | 8 +- src/codespy/agents/reviewer/reviewer.py | 80 +++++++++---------- src/codespy/agents/reviewer/server.py | 10 +-- src/codespy/cli_remote.py | 14 ++-- src/codespy/tools/__init__.py | 4 +- src/codespy/tools/git/__init__.py | 8 +- src/codespy/tools/git/base.py | 20 ++--- src/codespy/tools/git/client.py | 4 +- src/codespy/tools/git/github_client.py | 8 +- src/codespy/tools/git/gitlab_client.py | 26 +++--- src/codespy/tools/git/local_diff.py | 18 ++--- src/codespy/tools/git/models.py | 40 ++++------ src/codespy/tools/git/server.py | 26 +++--- 20 files changed, 210 insertions(+), 222 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 4df540c..9320564 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -109,7 +109,7 @@ Or for AWS Bedrock: **Available MCP Tools:** - `review_local_changes(repo_path, base_ref)` — Review branch changes vs base (e.g., vs `main`) - `review_uncommitted(repo_path)` — Review staged + unstaged working tree changes -- `review_pr(mr_url)` — Review a GitHub PR or GitLab MR by URL +- `review_pr(pr_url)` — Review a GitHub PR or GitLab MR by URL Then ask your AI assistant: *"Review my local changes"* or *"Review uncommitted work in /path/to/repo"* diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index c2fec34..0adec47 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -18,8 +18,8 @@ class PRContext(BaseModel): """ repo_slug: str = Field(description="Host-qualified repo identifier (e.g. github.com/owner/repo)") - mr_number: int = Field(description="MR/PR number") - mr_title: str = Field(description="MR/PR title") + pr_number: int = Field(description="PR number") + pr_title: str = Field(description="PR title") summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") @@ -65,19 +65,19 @@ class PackageManifest(BaseModel): package_name: str | None = Field(default=None, description="Package identity from manifest") -from codespy.tools.git.models import ChangedFile, MergeRequest +from codespy.tools.git.models import ChangedFile, PullRequest class ReviewMetadata(BaseModel): """Runtime pipeline state, stable once constructed at pipeline start. - Groups repo_path, run_id, mr, and is_local to reduce parameter + Groups repo_path, run_id, pr, and is_local to reduce parameter proliferation across module method signatures. """ repo_path: Path run_id: str | None = None - mr: MergeRequest | None = None + pr: PullRequest | None = None is_local: bool = False @@ -204,11 +204,11 @@ def tokens_per_call(self) -> float: class ReviewResult(BaseModel): - """Complete review results for a merge request (GitHub PR or GitLab MR).""" + """Complete review results for a pull request (GitHub PR or GitLab MR).""" - mr_number: int = Field(description="MR number") - mr_title: str = Field(description="MR title") - mr_url: str = Field(description="MR URL") + pr_number: int = Field(description="PR number") + pr_title: str = Field(description="PR title") + pr_url: str = Field(description="PR URL") repo: str = Field(description="Repository name (owner/repo)") run_id: str = Field( default="", @@ -279,9 +279,9 @@ def issues_by_severity(self) -> dict[IssueSeverity, list[Issue]]: def to_markdown(self) -> str: """Format review results as Markdown.""" lines = [ - f"# Code Review: {self.mr_title}", + f"# Code Review: {self.pr_title}", "", - f"**MR:** [{self.repo}#{self.mr_number}]({self.mr_url})", + f"**PR:** [{self.repo}#{self.pr_number}]({self.pr_url})", f"**Reviewed at:** {self.reviewed_at.strftime('%Y-%m-%d %H:%M UTC')}", f"**Model:** {self.model_used}", "", diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index ab7e449..3d21e64 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -21,7 +21,7 @@ class AuditSignature(dspy.Signature): - """Assess code quality and provide a recommendation for a merge request. + """Assess code quality and provide a recommendation for a pull request. You are a busy Principal Engineer. Be extremely terse. State facts only. Based on the summary, changed files, and issues found during review, provide: @@ -31,8 +31,8 @@ class AuditSignature(dspy.Signature): No polite filler. No conversational language. """ - mr_title: str = dspy.InputField(desc="Title of the merge request") - summary: str = dspy.InputField(desc="Summary of what this MR accomplishes") + pr_title: str = dspy.InputField(desc="Title of the pull request") + summary: str = dspy.InputField(desc="Summary of what this PR accomplishes") changed_files: list[ChangedFile] = dspy.InputField( desc="In-scope reviewable files with status and line counts" ) @@ -69,8 +69,8 @@ def _call_auditor( """Execute the auditor predictor (with or without Hippocampus memory).""" question = ( f"final audit of {review_context.pr_context.repo_slug}: " - f"pull request {review_context.pr_context.mr_number} " - f"{review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + f"pull request {review_context.pr_context.pr_number} " + f"{review_context.pr_context.pr_title}: {review_context.pr_context.summary}" ) if self._settings.get_memory_enabled("audit"): @@ -85,7 +85,7 @@ def _call_auditor( topic_ids=topic_ids, ) result = mem( - mr_title=review_context.pr_context.mr_title, + pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, changed_files=audit_files, all_issues=all_issues, @@ -108,7 +108,7 @@ def _call_auditor( mem.save_episode(store, path) else: result = auditor( - mr_title=review_context.pr_context.mr_title, + pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, changed_files=audit_files, all_issues=all_issues, diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 491e153..74b778c 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -11,7 +11,7 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.tools.git.models import MergeRequest + from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -199,7 +199,7 @@ async def aforward( # Local bindings from review_context metadata repo_path = review_context.metadata.repo_path run_id = review_context.metadata.run_id - mr = review_context.metadata.mr + pr = review_context.metadata.pr if not self._settings.is_signature_enabled("code_review"): logger.debug("Skipping code_review: disabled") @@ -250,9 +250,9 @@ async def aforward( if self._settings.get_memory_enabled("code_review"): question = ( f"review code change of {scope.repo}: {scope.subroot}: " - f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + f"pull request {review_context.pr_context.pr_number} {review_context.pr_context.pr_title}: {review_context.pr_context.summary}" ) - topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] + topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] mem = Hippocampus( agent, budget=self._settings.get_memory_budget("code_review"), diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 8f03804..88d0084 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -11,7 +11,7 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.tools.git.models import MergeRequest + from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, @@ -132,7 +132,7 @@ async def aforward( # Local bindings from review_context metadata repo_path = review_context.metadata.repo_path run_id = review_context.metadata.run_id - mr = review_context.metadata.mr + pr = review_context.metadata.pr if not self._settings.is_signature_enabled("doc"): logger.debug("Skipping doc: disabled") @@ -184,9 +184,9 @@ async def aforward( if self._settings.get_memory_enabled("doc"): question = ( f"review documentation of {scope.repo}: {scope.subroot}: " - f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + f"pull request {review_context.pr_context.pr_number} {review_context.pr_context.pr_title}: {review_context.pr_context.summary}" ) - topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] + topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] mem = Hippocampus( reviewer, budget=self._settings.get_memory_budget("doc"), diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index 1ae56ac..df3ceac 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -31,7 +31,7 @@ from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.client import get_client -from codespy.tools.git.models import ChangedFile, MergeRequest, should_review_file +from codespy.tools.git.models import ChangedFile, PullRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server from codespy.agents.reviewer.modules.manifest_parser import ( extract_package_name, @@ -327,8 +327,8 @@ class ScopeRefinementSignature(dspy.Signature): orphan_files: list[str] = dspy.InputField( desc="Changed files not assigned to any candidate (may be empty list)" ) - mr_title: str = dspy.InputField(desc="PR title for intent context") - mr_description: str = dspy.InputField(desc="PR description for intent context") + pr_title: str = dspy.InputField(desc="PR title for intent context") + pr_description: str = dspy.InputField(desc="PR description for intent context") project_instructions: str = dspy.InputField( desc="Project coding guidelines and structure context from config files (AGENTS.md, .kilo/, etc.). May be empty." ) @@ -432,12 +432,12 @@ async def _create_tools(self, repo_path: Path) -> tuple[list[Any], list[Any]]: return tools, contexts async def _ensure_repo( - self, mr: MergeRequest, repo_path: Path, is_local: bool + self, pr: PullRequest, repo_path: Path, is_local: bool ) -> None: """Clone repo programmatically if not already on disk. Args: - mr: The merge request + pr: The pull request repo_path: Path where repo should be cloned is_local: If True, skip cloning (repo already on disk) """ @@ -448,7 +448,7 @@ async def _ensure_repo( if repo_path.exists() and (repo_path / ".git").exists(): from git import Repo logger.debug("Updating existing clone at %s", repo_path) - changed_file_paths = [f.filename for f in mr.changed_files] + changed_file_paths = [f.filename for f in pr.changed_files] sparse_paths = derive_sparse_paths(changed_file_paths) # Update sparse-checkout config sparse_file = repo_path / ".git" / "info" / "sparse-checkout" @@ -456,32 +456,32 @@ async def _ensure_repo( sparse_file.write_text("\n".join(sparse_paths) + "\n") # Fetch and checkout correct ref repo = Repo(repo_path) - repo.git.fetch("origin", mr.head_sha, "--depth", "1") - repo.git.checkout(mr.head_sha) + repo.git.fetch("origin", pr.head_sha, "--depth", "1") + repo.git.checkout(pr.head_sha) # Ensure manifests at root + parent dirs await self._ensure_manifests(repo_path, changed_file_paths) return - changed_file_paths = [f.filename for f in mr.changed_files] + changed_file_paths = [f.filename for f in pr.changed_files] sparse_paths = derive_sparse_paths(changed_file_paths) logger.info("Sparse checkout paths: %s", sparse_paths) # Build a dummy URL to get the right client - if mr.platform == "gitlab": + if pr.platform == "gitlab": gitlab_url = self._settings.gitlab_url.rstrip("/") - dummy_url = f"{gitlab_url}/{mr.repo_owner}/{mr.repo_name}/-/merge_requests/1" + dummy_url = f"{gitlab_url}/{pr.repo_owner}/{pr.repo_name}/-/merge_requests/1" else: - dummy_url = f"https://github.com/{mr.repo_owner}/{mr.repo_name}/pull/1" + dummy_url = f"https://github.com/{pr.repo_owner}/{pr.repo_name}/pull/1" client = get_client(dummy_url, self._settings) logger.info( - "Cloning %s/%s@%s...", mr.repo_owner, mr.repo_name, mr.head_sha[:8] + "Cloning %s/%s@%s...", pr.repo_owner, pr.repo_name, pr.head_sha[:8] ) client.clone_repository( - owner=mr.repo_owner, - repo_name=mr.repo_name, - ref=mr.head_sha, + owner=pr.repo_owner, + repo_name=pr.repo_name, + ref=pr.head_sha, target_path=repo_path, depth=1, sparse_paths=sparse_paths, @@ -925,7 +925,7 @@ async def _refine_scopes( ) # Local bindings from review_context metadata - mr = review_context.metadata.mr + pr = review_context.metadata.pr repo_path = review_context.metadata.repo_path run_id = review_context.metadata.run_id @@ -954,8 +954,8 @@ async def _refine_scopes( if self._settings.get_memory_enabled("scope"): question = ( f"refine scopes of {review_context.pr_context.repo_slug}: " - f"PR #{review_context.pr_context.mr_number} " - f"{review_context.pr_context.mr_title}: " + f"PR #{review_context.pr_context.pr_number} " + f"{review_context.pr_context.pr_title}: " f"{review_context.pr_context.summary}" ) mem = Hippocampus( @@ -970,16 +970,16 @@ async def _refine_scopes( result = await mem.aforward( candidates=candidates_str, orphan_files=[f.filename for f in orphans], - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", + pr_title=pr.title or "No title", + pr_description=pr.body or "No description", project_instructions=project_instructions, ) else: result = await agent.acall( candidates=candidates_str, orphan_files=[f.filename for f in orphans], - mr_title=mr.title or "No title", - mr_description=mr.body or "No description", + pr_title=pr.title or "No title", + pr_description=pr.body or "No description", project_instructions=project_instructions, ) @@ -988,7 +988,7 @@ async def _refine_scopes( # Apply LLM boundaries with manifest guardrail and deterministic file assignment final_scopes = self._apply_boundaries( - result.scopes, all_files, scopes, mr.repo_slug + result.scopes, all_files, scopes, pr.repo_slug ) # Copy ScopeBoundary.description to ScopeResult.description (overrides deterministic fallback) @@ -1002,7 +1002,7 @@ async def _refine_scopes( scope_topic_ids: dict[str, str] = {} for scope in final_scopes: pkg_name = scope.package_manifest.package_name if scope.package_manifest else None - tid = make_topic_id(mr.repo_full_name, scope.subroot, pkg_name) + tid = make_topic_id(pr.repo_full_name, scope.subroot, pkg_name) scope_topic_ids[scope.subroot] = tid if pkg_name: internal_packages[pkg_name] = tid @@ -1041,7 +1041,7 @@ async def _refine_scopes( # Compute common ancestor topic if >1 scope common_ancestor_topic_id = compute_common_ancestor_topic_id( - mr.repo_full_name, [s.subroot for s in final_scopes] + pr.repo_full_name, [s.subroot for s in final_scopes] ) if common_ancestor_topic_id: # Build description: "Common context for scopes: subroot1, subroot2, ..." @@ -1079,7 +1079,7 @@ async def _refine_scopes( # Persist episode at deepest common folder when memory is enabled if mem is not None: - common_dir = _deepest_common_folder(final_scopes, mr.repo_slug) + common_dir = _deepest_common_folder(final_scopes, pr.repo_slug) scope_desc = "\n".join( f"- {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" for s in final_scopes @@ -1103,7 +1103,7 @@ async def aforward( self, review_context: ReviewContext, ) -> tuple[list[ScopeResult], "ContextMemory | None"]: - """Resolve scopes in the repository for the given MR. + """Resolve scopes in the repository for the given PR. Args: review_context: Review context with inherited memory and metadata @@ -1112,16 +1112,16 @@ async def aforward( Tuple of (list of ScopeResult, final context memory or None) """ # Local bindings from review_context metadata - mr = review_context.metadata.mr + pr = review_context.metadata.pr repo_path = review_context.metadata.repo_path is_local = review_context.metadata.is_local run_id = review_context.metadata.run_id excluded_dirs = self._settings.excluded_directories - reviewable_files = [f for f in mr.changed_files if should_review_file(f, excluded_dirs)] + reviewable_files = [f for f in pr.changed_files if should_review_file(f, excluded_dirs)] if not reviewable_files: return [], review_context.memory - repo = mr.repo_slug + repo = pr.repo_slug if not self._settings.is_signature_enabled("scope"): fallback = ScopeResult( repo=repo, subroot=".", scope_type=ScopeType.APPLICATION, @@ -1130,11 +1130,11 @@ async def aforward( description="Repository root", ) fallback.skills = collect_skills(repo_path, ".") - root_topic = fallback.topic(mr.repo_full_name) + root_topic = fallback.topic(pr.repo_full_name) return [fallback], ContextMemory(topics=[root_topic]) try: - await self._ensure_repo(mr, repo_path, is_local) + await self._ensure_repo(pr, repo_path, is_local) scopes, orphans = self._resolve(repo_path, reviewable_files, repo) # Log deterministic scopes before LLM refinement if scopes: @@ -1155,12 +1155,12 @@ async def aforward( if self._settings.get_memory_enabled("scope"): from codespy.agents.memory.hippocampus.episode import find_latest_episode store = get_memory_store(self._settings) - common_dir = _deepest_common_folder(scopes, mr.repo_slug) if scopes else f"/{mr.repo_slug}/" + common_dir = _deepest_common_folder(scopes, pr.repo_slug) if scopes else f"/{pr.repo_slug}/" prior_episode = find_latest_episode(store, common_dir, task="scope", exclude_run_id=run_id) # Fallback: prior run may have persisted at repo root if scopes differed - if prior_episode is None and common_dir != f"/{mr.repo_slug}/": + if prior_episode is None and common_dir != f"/{pr.repo_slug}/": prior_episode = find_latest_episode( - store, f"/{mr.repo_slug}/", task="scope", exclude_run_id=run_id + store, f"/{pr.repo_slug}/", task="scope", exclude_run_id=run_id ) if prior_episode is not None: loaded_memory = prior_episode.context_memory @@ -1196,7 +1196,7 @@ async def aforward( f" - {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" for s in scopes ) - logger.info("Resolved %d scope(s) for %s:\n%s", len(scopes), mr.repo_slug, scope_summary) + logger.info("Resolved %d scope(s) for %s:\n%s", len(scopes), pr.repo_slug, scope_summary) return scopes, context_memory except Exception as e: @@ -1207,7 +1207,7 @@ async def aforward( reason=f"Fallback due to error: {e}", description="Repository root", ) - root_topic = fallback.topic(mr.repo_full_name) + root_topic = fallback.topic(pr.repo_full_name) return [fallback], ContextMemory(topics=[root_topic]) def forward( diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index db02157..0b0932a 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -19,24 +19,24 @@ class PRSummarySignature(dspy.Signature): - """Summarize what a merge request does in 2-3 sentences. + """Summarize what a pull request does in 2-3 sentences. You are a busy Principal Engineer. Be extremely terse. State facts only. Based on the title, description, changed file paths, and code patches, - describe what this MR accomplishes. No polite filler. No conversational language. + describe what this PR accomplishes. No polite filler. No conversational language. """ - mr_title: str = dspy.InputField(desc="Title of the merge request") - mr_description: str = dspy.InputField(desc="Description/body of the MR") + pr_title: str = dspy.InputField(desc="Title of the pull request") + pr_description: str = dspy.InputField(desc="Description/body of the PR") changed_file_paths: list[str] = dspy.InputField( - desc="List of changed file paths from the MR" + desc="List of changed file paths from the PR" ) patches: str = dspy.InputField( desc="Unified diff patches showing code changes. Each patch is prefixed with the filename." ) summary: str = dspy.OutputField( - desc="2-3 sentence summary of what this MR accomplishes" + desc="2-3 sentence summary of what this PR accomplishes" ) @@ -50,9 +50,9 @@ def __init__(self) -> None: def forward( self, - mr_title: str, - mr_description: str, - mr_number: int, + pr_title: str, + pr_description: str, + pr_number: int, changed_file_paths: list[str], patches: str, repo_slug: str, @@ -64,9 +64,9 @@ def forward( """Generate a PR summary. Args: - mr_title: Title of the merge request - mr_description: Description/body of the MR - mr_number: MR/PR number + pr_title: Title of the pull request + pr_description: Description/body of the PR + pr_number: PR number changed_file_paths: List of changed file paths patches: Unified diff patches showing code changes repo_slug: Host-qualified repo slug for episode path @@ -81,7 +81,7 @@ def forward( if not self._settings.is_signature_enabled("summary"): logger.debug("Skipping summary: disabled") - return mr_title or "No title", initial_memory + return pr_title or "No title", initial_memory # Load latest episode per scope and merge with inherited memory if self._settings.get_memory_enabled("summary") and scopes: from codespy.agents.memory.hippocampus.episode import find_latest_episode @@ -101,7 +101,7 @@ def forward( summarizer = ContextSafe(dspy.ChainOfThought(PRSummarySignature), PRSummarySignature, name="summary") logger.info("Generating PR summary...") - question = f"summarize {repo_slug}: pull request {mr_number} {mr_title}" + question = f"summarize {repo_slug}: pull request {pr_number} {pr_title}" mem: Hippocampus | None = None with SignatureContext("summary", self._cost_tracker): @@ -117,8 +117,8 @@ def forward( topic_ids=topic_ids, ) result = mem( - mr_title=mr_title, - mr_description=mr_description, + pr_title=pr_title, + pr_description=pr_description, changed_file_paths=changed_file_paths, patches=patches, ) @@ -129,8 +129,8 @@ def forward( ) else: result = summarizer( - mr_title=mr_title, - mr_description=mr_description, + pr_title=pr_title, + pr_description=pr_description, changed_file_paths=changed_file_paths, patches=patches, ) diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 43437e2..498afc2 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -11,7 +11,7 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult -from codespy.tools.git.models import MergeRequest + from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -260,7 +260,7 @@ async def aforward( # Local bindings from review_context metadata repo_path = review_context.metadata.repo_path run_id = review_context.metadata.run_id - mr = review_context.metadata.mr + pr = review_context.metadata.pr # Check if supply chain signature is enabled if not self._settings.is_signature_enabled("supply_chain"): @@ -336,9 +336,9 @@ async def aforward( if self._settings.get_memory_enabled("supply_chain"): question = ( f"review supply chain of {scope.repo}: {scope.subroot}: " - f"pull request {review_context.pr_context.mr_number} {review_context.pr_context.mr_title}: {review_context.pr_context.summary}" + f"pull request {review_context.pr_context.pr_number} {review_context.pr_context.pr_title}: {review_context.pr_context.summary}" ) - topic_ids = [scope.topic(mr.repo_full_name).id] if mr else [] + topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] mem = Hippocampus( supply_chain_agent, budget=self._settings.get_memory_budget("supply_chain"), diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index ea537b5..be3c650 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -11,8 +11,8 @@ from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access from codespy.config import Settings, get_settings from codespy.config_memory import verify_memory_access -from codespy.tools.git import GitClient, get_client, ChangedFile, MergeRequest -from codespy.tools.git.local_diff import build_mr_from_diff +from codespy.tools.git import GitClient, get_client, ChangedFile, PullRequest +from codespy.tools.git.local_diff import build_pr_from_diff from codespy.tools.git.patch_utils import compact_patches from codespy.agents.memory.hippocampus import ContextMemory from codespy.agents.reviewer.models import ( @@ -81,21 +81,21 @@ def _get_git_client(self, url: str) -> GitClient: self._git_client = get_client(url, self.settings) return self._git_client - def _fetch_mr(self, mr_url: str) -> MergeRequest: - """Fetch merge request data from Git platform.""" - client = self._get_git_client(mr_url) - logger.info(f"Fetching MR data from {client.platform_name}...") - mr = client.fetch_merge_request(mr_url) - logger.info(f"MR #{mr.number}: {mr.title} ({len(mr.changed_files)} files)") - return mr + def _fetch_pr(self, pr_url: str) -> PullRequest: + """Fetch pull request data from Git platform.""" + client = self._get_git_client(pr_url) + logger.info(f"Fetching PR data from {client.platform_name}...") + pr = client.fetch_pull_request(pr_url) + logger.info(f"PR #{pr.number}: {pr.title} ({len(pr.changed_files)} files)") + return pr - def _get_repo_path(self, mr: MergeRequest) -> Path: + def _get_repo_path(self, pr: PullRequest) -> Path: """Get the local repository path for a MR, creating directories if needed.""" cache_dir = self.settings.cache_dir cache_dir.mkdir(parents=True, exist_ok=True) # Handle nested namespaces for GitLab - owner_path = mr.repo_owner.replace("/", "_") - return cache_dir / owner_path / mr.repo_name + owner_path = pr.repo_owner.replace("/", "_") + return cache_dir / owner_path / pr.repo_name async def _run_review_modules( self, @@ -135,17 +135,17 @@ async def _run_review_modules( context_memories[module_names[i]] = ctx_mem return all_issues, context_memories - def _build_local_mr(self, config: LocalReviewConfig) -> MergeRequest: - """Build a MergeRequest from local git changes. + def _build_local_pr(self, config: LocalReviewConfig) -> PullRequest: + """Build a PullRequest from local git changes. Args: config: Local review configuration Returns: - MergeRequest object built from local git changes + PullRequest object built from local git changes """ - logger.info(f"Building MR from local changes in {config.repo_path}...") - return build_mr_from_diff( + logger.info(f"Building PR from local changes in {config.repo_path}...") + return build_pr_from_diff( repo_path=config.repo_path, base_ref=config.base_ref, include_uncommitted=config.uncommitted @@ -172,17 +172,17 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # Verify memory storage access self._verify_memory_access() - # Determine mode and fetch/build MR accordingly + # Determine mode and fetch/build PR accordingly if isinstance(config, RemoteReviewConfig): # Remote mode: fetch from GitHub/GitLab logger.info(f"Starting review of {config.url}") - mr = self._fetch_mr(config.url) - repo_path = self._get_repo_path(mr) + pr = self._fetch_pr(config.url) + repo_path = self._get_repo_path(pr) elif isinstance(config, LocalReviewConfig): - # Local mode: build MR from local git changes + # Local mode: build PR from local git changes mode = "uncommitted changes" if config.uncommitted else f"changes vs {config.base_ref}" logger.info(f"Starting local review: {mode} in {config.repo_path}") - mr = self._build_local_mr(config) + pr = self._build_local_pr(config) repo_path = config.repo_path.resolve() else: raise ValueError(f"Invalid config type: {type(config)}") @@ -191,12 +191,12 @@ def forward(self, config: ReviewConfig) -> ReviewResult: is_local = isinstance(config, LocalReviewConfig) logger.info("Identifying code scopes...") pr_context = PRContext( - repo_slug=mr.repo_slug, - mr_number=mr.number, - mr_title=mr.title, - summary=mr.title, # Use title as placeholder since summary hasn't run + repo_slug=pr.repo_slug, + pr_number=pr.number, + pr_title=pr.title, + summary=pr.title, # Use title as placeholder since summary hasn't run ) - metadata = ReviewMetadata(repo_path=repo_path, run_id=run_id, mr=mr, is_local=is_local) + metadata = ReviewMetadata(repo_path=repo_path, run_id=run_id, pr=pr, is_local=is_local) review_ctx = ReviewContext(pr_context=pr_context, memory=None, metadata=metadata) scopes, initial_memory = self.scope_resolver(review_context=review_ctx) for scope in scopes: @@ -213,19 +213,19 @@ def forward(self, config: ReviewConfig) -> ReviewResult: self._expand_sparse_for_scopes(scopes, repo_path) # Compact patches: expand context to function bodies for better review context logger.info("Compacting patches to function boundaries...") - changed_file_paths = [f.filename for f in mr.changed_files] - patches = build_patches(mr.changed_files) + changed_file_paths = [f.filename for f in pr.changed_files] + patches = build_patches(pr.changed_files) compact_patches(scopes, repo_path) # Step 2: Run Summarizer (now receives scopes for per-scope episode persistence) # Compute all scope topic IDs for summarizer - all_scope_topic_ids = [s.topic(mr.repo_full_name).id for s in scopes] + all_scope_topic_ids = [s.topic(pr.repo_full_name).id for s in scopes] pr_summary, summarizer_memory = self.summarizer( - mr_title=mr.title, - mr_description=mr.body or "No description provided.", - mr_number=mr.number, + pr_title=pr.title, + pr_description=pr.body or "No description provided.", + pr_number=pr.number, changed_file_paths=changed_file_paths, patches=patches, - repo_slug=mr.repo_slug, + repo_slug=pr.repo_slug, run_id=run_id, scopes=scopes, initial_memory=initial_memory, @@ -249,23 +249,21 @@ def forward(self, config: ReviewConfig) -> ReviewResult: scoped_files = self._collect_scoped_files(scopes) logger.info( f"Audit input: {len(scoped_files)} in-scope files " - f"(filtered from {len(mr.changed_files)} total)" + f"(filtered from {len(pr.changed_files)} total)" ) quality_assessment, recommendation = self.auditor( review_context=review_ctx, changed_files=scoped_files, all_issues=all_issues, run_id=run_id, - scopes=scopes, - topic_ids=all_scope_topic_ids, ) # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() return ReviewResult( - mr_number=mr.number, - mr_title=mr.title, - mr_url=mr.url, - repo=mr.repo_full_name, + pr_number=pr.number, + pr_title=pr.title, + pr_url=pr.url, + repo=pr.repo_full_name, run_id=run_id, model_used=self.settings.default_model, issues=all_issues, diff --git a/src/codespy/agents/reviewer/server.py b/src/codespy/agents/reviewer/server.py index e38ce98..819ee6f 100644 --- a/src/codespy/agents/reviewer/server.py +++ b/src/codespy/agents/reviewer/server.py @@ -77,13 +77,13 @@ def _do_uncommitted_review(repo_path: str, output_format: str) -> str: return result.to_markdown() -def _do_pr_review(mr_url: str, output_format: str) -> str: +def _do_pr_review(pr_url: str, output_format: str) -> str: """Synchronous PR review — runs in thread pool.""" import json from codespy.agents.reviewer.models import RemoteReviewConfig - config = RemoteReviewConfig(url=mr_url) + config = RemoteReviewConfig(url=pr_url) pipeline = _get_pipeline() result = pipeline(config) @@ -154,7 +154,7 @@ async def review_uncommitted( @mcp.tool() async def review_pr( - mr_url: str, + pr_url: str, output_format: str = "markdown", ) -> str: """Review a GitHub Pull Request or GitLab Merge Request by URL. @@ -163,7 +163,7 @@ async def review_pr( the full codespy review pipeline. Args: - mr_url: Full URL of the PR/MR to review. + pr_url: Full URL of the PR/MR to review. GitHub: https://github.com/owner/repo/pull/123 GitLab: https://gitlab.com/namespace/project/-/merge_requests/123 output_format: Output format — "markdown" for human-readable or "json" for structured data. @@ -173,7 +173,7 @@ async def review_pr( Review results as markdown or JSON string """ try: - return await _run_in_thread(_do_pr_review, mr_url, output_format) + return await _run_in_thread(_do_pr_review, pr_url, output_format) except Exception as e: logger.exception("Review failed") return f"Review failed: {e}" diff --git a/src/codespy/cli_remote.py b/src/codespy/cli_remote.py index e93823e..c549aeb 100644 --- a/src/codespy/cli_remote.py +++ b/src/codespy/cli_remote.py @@ -16,10 +16,10 @@ def review( - mr_url: Annotated[ + pr_url: Annotated[ str, typer.Argument( - help="Merge request URL (GitHub PR or GitLab MR)", + help="Pull request URL (GitHub PR or GitLab MR)", ), ], config_file: Annotated[ @@ -102,7 +102,7 @@ def review( settings.output_git = git_comment # Validate URL format - if not is_supported_url(mr_url): + if not is_supported_url(pr_url): console.print( "[red]Error:[/red] Unsupported URL format.\n\n" "Supported formats:\n" @@ -112,7 +112,7 @@ def review( raise typer.Exit(1) # Detect platform and validate token - platform = detect_platform(mr_url) + platform = detect_platform(pr_url) if platform == "github": token = settings.github_token @@ -146,7 +146,7 @@ def review( console.print( Panel( - f"[bold blue]Reviewing MR:[/bold blue] {mr_url}\n" + f"[bold blue]Reviewing PR:[/bold blue] {pr_url}\n" f"[bold]Platform:[/bold] {platform.title()}\n" f"[bold]Model:[/bold] {settings.default_model}\n" f"[bold]Output:[/bold] {output_display}\n" @@ -162,7 +162,7 @@ def review( pipeline = ReviewPipeline(settings) # Create remote review config - config = RemoteReviewConfig(url=mr_url) + config = RemoteReviewConfig(url=pr_url) # Run review (model access always verified in pipeline) result = pipeline(config) @@ -188,7 +188,7 @@ def review( if settings.output_git: console.print(f"[dim]Posting review to {platform.title()}...[/dim]") - git_reporter = GitReporter(url=mr_url, settings=settings) + git_reporter = GitReporter(url=pr_url, settings=settings) git_reporter.report(result) console.print(f"[green]✓[/green] {platform.title()} review posted successfully") diff --git a/src/codespy/tools/__init__.py b/src/codespy/tools/__init__.py index fa7f013..f276315 100644 --- a/src/codespy/tools/__init__.py +++ b/src/codespy/tools/__init__.py @@ -5,7 +5,7 @@ from codespy.tools.git import ( ChangedFile, GitClient, - MergeRequest, + PullRequest, detect_platform, get_client, ) @@ -23,7 +23,7 @@ "get_client", "detect_platform", "ChangedFile", - "MergeRequest", + "PullRequest", "OSVClient", "Vulnerability", "ScanResult", diff --git a/src/codespy/tools/git/__init__.py b/src/codespy/tools/git/__init__.py index 78327a3..853814f 100644 --- a/src/codespy/tools/git/__init__.py +++ b/src/codespy/tools/git/__init__.py @@ -2,13 +2,13 @@ from codespy.tools.git.base import GitClient from codespy.tools.git.client import detect_platform, get_client, is_supported_url -from codespy.tools.git.local_diff import build_mr_from_diff +from codespy.tools.git.local_diff import build_pr_from_diff from codespy.tools.git.models import ( CallerInfo, ChangedFile, FileStatus, GitPlatform, - MergeRequest, + PullRequest, ReviewContext, should_review_file, ) @@ -21,9 +21,9 @@ "get_client", "detect_platform", "is_supported_url", - "build_mr_from_diff", + "build_pr_from_diff", "GitPlatform", - "MergeRequest", + "PullRequest", "ChangedFile", "FileStatus", "ReviewContext", diff --git a/src/codespy/tools/git/base.py b/src/codespy/tools/git/base.py index 8b93d07..bee9560 100644 --- a/src/codespy/tools/git/base.py +++ b/src/codespy/tools/git/base.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from codespy.config import Settings - from codespy.tools.git.models import MergeRequest + from codespy.tools.git.models import PullRequest class GitClient(ABC): @@ -24,13 +24,13 @@ def __init__(self, settings: "Settings | None" = None) -> None: @abstractmethod def parse_url(self, url: str) -> tuple[str, str, int]: - """Parse a merge request URL into owner, repo, and MR number. + """Parse a pull request URL into owner, repo, and PR number. Args: - url: Merge request URL + url: Pull request URL Returns: - Tuple of (owner, repo, mr_number) + Tuple of (owner, repo, pr_number) Raises: ValueError: If URL is not valid for this platform @@ -38,14 +38,14 @@ def parse_url(self, url: str) -> tuple[str, str, int]: ... @abstractmethod - def fetch_merge_request(self, url: str) -> "MergeRequest": - """Fetch merge request data from the platform. + def fetch_pull_request(self, url: str) -> "PullRequest": + """Fetch pull request data from the platform. Args: - url: Merge request URL + url: Pull request URL Returns: - MergeRequest model with all data + PullRequest model with all data """ ... @@ -82,10 +82,10 @@ def submit_review( comments: list[dict] | None = None, commit_sha: str | None = None, ) -> None: - """Submit a review on a merge request. + """Submit a review on a pull request. Args: - url: Merge request URL + url: Pull request URL body: Review body/summary text comments: List of inline comment dicts with keys: - path: File path diff --git a/src/codespy/tools/git/client.py b/src/codespy/tools/git/client.py index f25ddb9..01d93b8 100644 --- a/src/codespy/tools/git/client.py +++ b/src/codespy/tools/git/client.py @@ -26,7 +26,7 @@ def get_client(url: str, settings: "Settings | None" = None) -> GitClient: and returns the appropriate client instance. Args: - url: Merge request URL (GitHub PR or GitLab MR) + url: Pull request URL (GitHub PR or GitLab MR) settings: Application settings. Uses global settings if not provided. Returns: @@ -53,7 +53,7 @@ def detect_platform(url: str) -> str: """Detect the Git platform from a URL. Args: - url: Merge request URL + url: Pull request URL Returns: Platform name ('github' or 'gitlab') diff --git a/src/codespy/tools/git/github_client.py b/src/codespy/tools/git/github_client.py index 61c194d..039cdbb 100644 --- a/src/codespy/tools/git/github_client.py +++ b/src/codespy/tools/git/github_client.py @@ -13,7 +13,7 @@ ChangedFile, FileStatus, GitPlatform, - MergeRequest, + PullRequest, ) logger = logging.getLogger(__name__) @@ -72,14 +72,14 @@ def parse_url(self, url: str) -> tuple[str, str, int]: ) return match.group("owner"), match.group("repo"), int(match.group("number")) - def fetch_merge_request(self, url: str) -> MergeRequest: + def fetch_pull_request(self, url: str) -> PullRequest: """Fetch pull request data from GitHub. Args: url: GitHub PR URL Returns: - MergeRequest model with all data + PullRequest model with all data """ owner, repo_name, pr_number = self.parse_url(url) @@ -102,7 +102,7 @@ def fetch_merge_request(self, url: str) -> MergeRequest: ) ) - return MergeRequest( + return PullRequest( number=gh_pr.number, title=gh_pr.title, body=gh_pr.body, diff --git a/src/codespy/tools/git/gitlab_client.py b/src/codespy/tools/git/gitlab_client.py index 64c4213..cf17f77 100644 --- a/src/codespy/tools/git/gitlab_client.py +++ b/src/codespy/tools/git/gitlab_client.py @@ -12,7 +12,7 @@ ChangedFile, FileStatus, GitPlatform, - MergeRequest, + PullRequest, ) logger = logging.getLogger(__name__) @@ -65,7 +65,7 @@ def parse_url(self, url: str) -> tuple[str, str, int]: url: GitLab MR URL Returns: - Tuple of (namespace, project, mr_number) + Tuple of (namespace, project, pr_number) Note: namespace may contain slashes for nested groups Raises: @@ -79,7 +79,7 @@ def parse_url(self, url: str) -> tuple[str, str, int]: ) path = match.group("path") - mr_number = int(match.group("number")) + pr_number = int(match.group("number")) # Split path into namespace and project # Handle nested namespaces (e.g., group/subgroup/project) @@ -90,7 +90,7 @@ def parse_url(self, url: str) -> tuple[str, str, int]: namespace = "" project = parts[0] - return namespace, project, mr_number + return namespace, project, pr_number def _get_project_path(self, url: str) -> str: """Get the full project path from URL.""" @@ -115,22 +115,22 @@ def _map_status(self, diff_status: str) -> FileStatus: } return status_map.get(diff_status, FileStatus.MODIFIED) - def fetch_merge_request(self, url: str) -> MergeRequest: + def fetch_pull_request(self, url: str) -> PullRequest: """Fetch merge request data from GitLab. Args: url: GitLab MR URL Returns: - MergeRequest model with all data + PullRequest model with all data """ - namespace, project_name, mr_number = self.parse_url(url) + namespace, project_name, pr_number = self.parse_url(url) project_path = self._get_project_path(url) host = self._get_host(url) # Get project and MR project = self.gitlab_client.projects.get(project_path) - gl_mr = project.mergerequests.get(mr_number) + gl_mr = project.mergerequests.get(pr_number) # Get diff/changes changes = gl_mr.changes() @@ -168,7 +168,7 @@ def fetch_merge_request(self, url: str) -> MergeRequest: state_map = {"opened": "open", "closed": "closed", "merged": "merged"} state = state_map.get(gl_mr.state, gl_mr.state) - return MergeRequest( + return PullRequest( number=gl_mr.iid, title=gl_mr.title, body=gl_mr.description, @@ -292,10 +292,10 @@ def submit_review( commit_sha: Commit SHA to review (defaults to head SHA) """ project_path = self._get_project_path(url) - _, _, mr_number = self.parse_url(url) + _, _, pr_number = self.parse_url(url) project = self.gitlab_client.projects.get(project_path) - gl_mr = project.mergerequests.get(mr_number) + gl_mr = project.mergerequests.get(pr_number) # Get changes and commit SHA for positioning changes = gl_mr.changes() @@ -387,11 +387,11 @@ def submit_review( # Post main review body as a note (with any failed comments appended) if final_body: gl_mr.notes.create({"body": final_body}) - logger.info(f"Posted review on {project_path}!{mr_number}") + logger.info(f"Posted review on {project_path}!{pr_number}") if comments: logger.info( - f"Submitted {successful_count}/{len(comments)} inline comments on {project_path}!{mr_number}" + f"Submitted {successful_count}/{len(comments)} inline comments on {project_path}!{pr_number}" ) def _append_comments_to_body(self, body: str, comments: list[dict]) -> str: diff --git a/src/codespy/tools/git/local_diff.py b/src/codespy/tools/git/local_diff.py index f8acfb1..b6a26d3 100644 --- a/src/codespy/tools/git/local_diff.py +++ b/src/codespy/tools/git/local_diff.py @@ -1,11 +1,11 @@ -"""Build MergeRequest objects from local git state (no GitHub/GitLab needed).""" +"""Build PullRequest objects from local git state (no GitHub/GitLab needed).""" import logging import subprocess from datetime import UTC, datetime from pathlib import Path -from codespy.tools.git.models import ChangedFile, FileStatus, GitPlatform, MergeRequest +from codespy.tools.git.models import ChangedFile, FileStatus, GitPlatform, PullRequest logger = logging.getLogger(__name__) @@ -100,12 +100,12 @@ def _get_current_user(repo_path: Path) -> str: return "local-user" -def build_mr_from_diff( +def build_pr_from_diff( repo_path: Path, base_ref: str = "main", include_uncommitted: bool = False, -) -> MergeRequest: - """Build a MergeRequest from local git diff. +) -> PullRequest: + """Build a PullRequest from local git diff. Args: repo_path: Path to the local git repository @@ -114,7 +114,7 @@ def build_mr_from_diff( If False, diff current branch against base_ref. Returns: - A MergeRequest object representing the local changes + A PullRequest object representing the local changes Raises: RuntimeError: If git commands fail @@ -153,7 +153,7 @@ def build_mr_from_diff( name_status_output = _run_git(repo_path, "diff", "--name-status", diff_ref) if not name_status_output: logger.info("No changes found") - return MergeRequest( + return PullRequest( number=0, title=title, body=f"Local diff: {diff_ref}...HEAD", @@ -205,9 +205,9 @@ def build_mr_from_diff( previous_filename=previous_filename, )) - logger.info(f"Built local MR with {len(changed_files)} changed files") + logger.info(f"Built local PR with {len(changed_files)} changed files") - return MergeRequest( + return PullRequest( number=0, title=title, body=f"Local diff: {diff_ref}...HEAD in {repo_path}", diff --git a/src/codespy/tools/git/models.py b/src/codespy/tools/git/models.py index 667b6c1..e521adb 100644 --- a/src/codespy/tools/git/models.py +++ b/src/codespy/tools/git/models.py @@ -1,4 +1,4 @@ -"""Data models for Git merge requests (GitHub PRs and GitLab MRs).""" +"""Data models for Git pull requests (GitHub PRs and GitLab MRs).""" import re from datetime import datetime @@ -8,7 +8,7 @@ class FileStatus(str, Enum): - """Status of a file in a merge request.""" + """Status of a file in a pull request.""" ADDED = "added" MODIFIED = "modified" @@ -59,7 +59,7 @@ class GitPlatform(str, Enum): class ChangedFile(BaseModel): - """Represents a file changed in a merge request.""" + """Represents a file changed in a pull request.""" filename: str = Field(description="Path to the file") status: FileStatus = Field(description="Type of change (added, modified, removed, renamed)") @@ -223,20 +223,20 @@ def should_review_file(file: ChangedFile, excluded_directories: list[str]) -> bo return True -class MergeRequest(BaseModel): - """Represents a merge request (GitHub PR or GitLab MR).""" +class PullRequest(BaseModel): + """Represents a pull request (GitHub PR or GitLab MR).""" - number: int = Field(description="MR/PR number") - title: str = Field(description="MR/PR title") - body: str | None = Field(default=None, description="MR/PR description/body") - state: str = Field(description="MR/PR state (open, closed, merged)") - author: str = Field(description="MR/PR author username") + number: int = Field(description="PR number") + title: str = Field(description="PR title") + body: str | None = Field(default=None, description="PR description/body") + state: str = Field(description="PR state (open, closed, merged)") + author: str = Field(description="PR author username") base_branch: str = Field(description="Target branch") head_branch: str = Field(description="Source branch") base_sha: str = Field(description="Base commit SHA") head_sha: str = Field(description="Head commit SHA") - created_at: datetime = Field(description="MR/PR creation timestamp") - updated_at: datetime = Field(description="MR/PR last update timestamp") + created_at: datetime = Field(description="PR creation timestamp") + updated_at: datetime = Field(description="PR last update timestamp") repo_owner: str = Field(description="Repository owner/namespace") repo_name: str = Field(description="Repository name") host: str = Field( @@ -249,7 +249,7 @@ class MergeRequest(BaseModel): changed_files: list[ChangedFile] = Field( default_factory=list, description="List of changed files" ) - labels: list[str] = Field(default_factory=list, description="MR/PR labels") + labels: list[str] = Field(default_factory=list, description="PR labels") platform: GitPlatform = Field(description="Git platform (github, gitlab)") @property @@ -271,7 +271,7 @@ def repo_slug(self) -> str: @property def url(self) -> str: - """Get the MR/PR URL.""" + """Get the PR URL.""" if self.platform == GitPlatform.GITLAB: return f"https://gitlab.com/{self.repo_full_name}/-/merge_requests/{self.number}" return f"https://github.com/{self.repo_full_name}/pull/{self.number}" @@ -287,10 +287,6 @@ def code_files(self) -> list[ChangedFile]: return [f for f in self.changed_files if f.is_code_file] -# Alias for backward compatibility -PullRequest = MergeRequest - - class CallerInfo(BaseModel): """Information about a caller of a function/method.""" @@ -303,7 +299,7 @@ class CallerInfo(BaseModel): class ReviewContext(BaseModel): """Context information for code review.""" - merge_request: MergeRequest = Field(description="The merge request being reviewed") + pull_request: PullRequest = Field(description="The pull request being reviewed") related_files: dict[str, str] = Field( default_factory=dict, description="Related files content (imports, dependencies)", @@ -316,12 +312,6 @@ class ReviewContext(BaseModel): description="Callers of changed functions, keyed by filename", ) - # Alias for backward compatibility - @property - def pull_request(self) -> MergeRequest: - """Alias for merge_request (backward compatibility).""" - return self.merge_request - def get_context_for_file(self, filename: str) -> str: """Get context string for a specific file.""" context_parts = [] diff --git a/src/codespy/tools/git/server.py b/src/codespy/tools/git/server.py index 5533c31..4778f93 100644 --- a/src/codespy/tools/git/server.py +++ b/src/codespy/tools/git/server.py @@ -27,47 +27,47 @@ def _get_client(url: str) -> GitClient: @mcp.tool() -def parse_mr_url(url: str) -> dict: - """Parse a Git merge request URL into owner, repo, and MR number. +def parse_pr_url(url: str) -> dict: + """Parse a Git pull request URL into owner, repo, and PR number. Works with both GitHub Pull Requests and GitLab Merge Requests. Args: - url: Git MR URL + url: Git PR/MR URL - GitHub: https://github.com/owner/repo/pull/123 - GitLab: https://gitlab.com/group/project/-/merge_requests/123 Returns: - Dict with owner (or namespace), repo, mr_number, and platform + Dict with owner (or namespace), repo, pr_number, and platform """ platform = detect_platform(url) client = _get_client(url) - owner, repo, mr_number = client.parse_url(url) + owner, repo, pr_number = client.parse_url(url) return { "owner": owner, "repo": repo, - "mr_number": mr_number, + "pr_number": pr_number, "platform": platform.value, } @mcp.tool() -def fetch_merge_request(mr_url: str) -> dict: - """Fetch merge request data from GitHub or GitLab. +def fetch_pull_request(pr_url: str) -> dict: + """Fetch pull request data from GitHub or GitLab. Works with both GitHub Pull Requests and GitLab Merge Requests. Args: - mr_url: Git MR URL + pr_url: Git PR/MR URL - GitHub: https://github.com/owner/repo/pull/123 - GitLab: https://gitlab.com/group/project/-/merge_requests/123 Returns: - Dict with MR data including title, body, changed files, etc. + Dict with PR data including title, body, changed files, etc. """ - client = _get_client(mr_url) - mr = client.fetch_merge_request(mr_url) - return mr.model_dump() + client = _get_client(pr_url) + pr = client.fetch_pull_request(pr_url) + return pr.model_dump() @mcp.tool() From ce33ba032cc7c4ff65264ed08fc2fc2ef111c8b3 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 23:36:12 +0200 Subject: [PATCH 74/79] wip --- docs/architecture.md | 2 +- docs/development.md | 1 + docs/usage.md | 4 ++ src/codespy/agents/context_safe.py | 1 - src/codespy/agents/cost_tracker.py | 30 +++++++-------- src/codespy/agents/dspy_config.py | 33 +++++++--------- .../memory/hippocampus/context_memory.py | 8 ++-- .../agents/memory/hippocampus/episode.py | 4 +- .../agents/memory/hippocampus/hippocampus.py | 9 ++++- .../memory/hippocampus/modules/distiller.py | 1 - src/codespy/agents/reviewer/models.py | 12 +++--- .../agents/reviewer/modules/auditor.py | 5 ++- .../agents/reviewer/modules/code_reviewer.py | 5 +-- .../agents/reviewer/modules/doc_reviewer.py | 4 +- .../agents/reviewer/modules/helpers.py | 12 +++--- .../reviewer/modules/manifest_parser.py | 38 +++++++++---------- .../agents/reviewer/modules/scope_resolver.py | 22 ++++++----- .../reviewer/modules/supply_chain_auditor.py | 4 +- .../agents/reviewer/reporters/__init__.py | 2 +- src/codespy/agents/reviewer/reporters/base.py | 2 +- src/codespy/agents/reviewer/reporters/git.py | 2 +- .../agents/reviewer/reporters/stdout.py | 2 +- src/codespy/agents/reviewer/reviewer.py | 25 ++++++------ src/codespy/cli.py | 8 ++-- src/codespy/cli_local.py | 16 ++++---- src/codespy/cli_remote.py | 8 ++-- src/codespy/config.py | 4 +- src/codespy/config_dspy.py | 1 - src/codespy/config_git.py | 2 +- src/codespy/config_io.py | 2 +- src/codespy/config_memory.py | 1 - src/codespy/tools/__init__.py | 2 +- src/codespy/tools/cyber/__init__.py | 2 +- src/codespy/tools/cyber/osv/__init__.py | 4 +- src/codespy/tools/cyber/osv/client.py | 5 +-- src/codespy/tools/cyber/osv/models.py | 10 ++--- src/codespy/tools/cyber/osv/server.py | 2 +- src/codespy/tools/git/base.py | 2 +- src/codespy/tools/git/client.py | 2 +- src/codespy/tools/git/models.py | 30 ++++++--------- src/codespy/tools/git/patch_utils.py | 16 +++----- src/codespy/tools/git/server.py | 15 ++++---- src/codespy/tools/mcp_utils.py | 4 +- src/codespy/tools/parsers/ripgrep/__init__.py | 2 +- src/codespy/tools/parsers/ripgrep/server.py | 2 +- .../tools/parsers/treesitter/__init__.py | 2 +- .../parsers/treesitter/base_extractor.py | 4 +- .../parsers/treesitter/extractors/cpp.py | 4 +- .../tools/parsers/treesitter/extractors/go.py | 2 +- .../parsers/treesitter/extractors/java.py | 2 +- .../treesitter/extractors/javascript.py | 2 +- .../parsers/treesitter/extractors/kotlin.py | 2 +- .../parsers/treesitter/extractors/objc.py | 2 +- .../parsers/treesitter/extractors/python.py | 2 +- .../treesitter/extractors/ripgrep_fallback.py | 12 ++---- .../parsers/treesitter/extractors/rust.py | 7 +--- .../parsers/treesitter/extractors/swift.py | 2 +- .../tools/parsers/treesitter/server.py | 2 +- src/codespy/tools/storage/base.py | 1 - .../tools/storage/filesystem/client.py | 4 +- .../tools/storage/filesystem/server.py | 1 - src/codespy/tools/storage/models.py | 6 +-- src/codespy/tools/web/__init__.py | 2 +- src/codespy/tools/web/client.py | 2 +- src/codespy/tools/web/models.py | 2 +- src/codespy/tools/web/server.py | 2 +- tests/test_config_memory.py | 2 - tests/test_context_memory.py | 4 +- tests/test_dspy_config.py | 12 ++---- tests/test_patch_utils.py | 2 - tests/test_s3_client.py | 9 +++-- tests/test_scope_resolver.py | 3 +- 72 files changed, 212 insertions(+), 253 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 5a5a919..7bccbf7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -114,7 +114,7 @@ See [Memory System](memory.md) for implementation details. ## Tools Layer -- **Filesystem**: `read_file`, `list_dir` +- **Filesystem**: `read_file`, `list_directory`, `find_files`, `search_text`, `get_file_summary` - **Git**: GitHub + GitLab clients, sparse checkout - **Parsers**: Ripgrep (code search) + Tree-sitter (multi-language AST) - **Web**: Browser-based web search diff --git a/docs/development.md b/docs/development.md index 5df5633..14b0c20 100644 --- a/docs/development.md +++ b/docs/development.md @@ -57,6 +57,7 @@ src/codespy/ ├── config_llm.py # LLM provider configuration ├── config_memory.py # Memory system configuration ├── agents/ # DSPy agents and pipeline +│ ├── context_safe.py # Context window overflow resilience (RLM fallback) │ ├── cost_tracker.py # Token/cost tracking │ ├── dspy_config.py # DSPy runtime config │ ├── memory/ # Hippocampus memory system diff --git a/docs/usage.md b/docs/usage.md index 9320564..f4d292b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -51,11 +51,15 @@ codespy review-local /path/to/repo # Review specific repo codespy review-local --base develop # Compare against develop codespy review-local --base origin/main # Compare against origin/main codespy review-local --base HEAD~5 # Compare against 5 commits back +codespy review-local --model anthropic/claude-sonnet-4-5-20250929 +codespy review-local --output json --config staging.yaml # Review uncommitted changes (staged + unstaged) codespy review-uncommitted # Review current dir codespy review-uncommitted /path/to/repo codespy review-uncommitted --output json +codespy review-uncommitted --model anthropic/claude-sonnet-4-5-20250929 +codespy review-uncommitted --config staging.yaml ``` ## IDE Integration (MCP Server) diff --git a/src/codespy/agents/context_safe.py b/src/codespy/agents/context_safe.py index 93c0670..82f2247 100644 --- a/src/codespy/agents/context_safe.py +++ b/src/codespy/agents/context_safe.py @@ -10,7 +10,6 @@ import dspy # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] - logger = logging.getLogger(__name__) # Regex for detecting context window overflow in error messages diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index b364141..a0042d6 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -11,7 +11,7 @@ from contextlib import AbstractContextManager from dataclasses import dataclass from types import TracebackType -from typing import Any, Optional +from typing import Any import dspy # type: ignore[import-untyped] @@ -26,8 +26,8 @@ class SignatureStats: cost: float = 0.0 tokens: int = 0 call_count: int = 0 - start_time: Optional[float] = None - end_time: Optional[float] = None + start_time: float | None = None + end_time: float | None = None @property def duration_seconds(self) -> float: @@ -50,7 +50,7 @@ def to_dict(self) -> dict: class CostTracker: """Track LLM costs across multiple calls with per-signature attribution. - + Uses DSPy's LM history for per-signature tracking, which works reliably even during parallel execution. """ @@ -67,7 +67,7 @@ def reset(self) -> None: def start_signature(self, signature_name: str) -> None: """Mark the start of a signature's execution. - + Args: signature_name: Name of the signature starting execution """ @@ -79,7 +79,7 @@ def start_signature(self, signature_name: str) -> None: def end_signature(self, signature_name: str, cost: float, tokens: int, call_count: int) -> None: """Mark the end of a signature's execution with its costs. - + Args: signature_name: Name of the signature ending execution cost: Total cost for this signature's LLM calls @@ -113,12 +113,12 @@ def call_count(self) -> int: with self._lock: return sum(s.call_count for s in self._signature_stats.values()) - def get_signature_stats(self, signature_name: str) -> Optional[SignatureStats]: + def get_signature_stats(self, signature_name: str) -> SignatureStats | None: """Get stats for a specific signature. - + Args: signature_name: Name of the signature - + Returns: SignatureStats or None if signature not found """ @@ -127,7 +127,7 @@ def get_signature_stats(self, signature_name: str) -> Optional[SignatureStats]: def get_all_signature_stats(self) -> dict[str, SignatureStats]: """Get stats for all signatures. - + Returns: Dictionary of signature name to SignatureStats """ @@ -145,7 +145,7 @@ def get_all_signature_stats(self) -> dict[str, SignatureStats]: def _get_history_entries() -> list[dict]: """Get current LM history entries from DSPy. - + Returns: List of history entries, or empty list if LM not configured """ @@ -160,7 +160,7 @@ def _get_history_entries() -> list[dict]: def _get_history_uuids() -> set[str]: """Get UUIDs of current history entries. - + Returns: Set of UUIDs from current history """ @@ -201,7 +201,7 @@ def _calculate_costs_from_entries(entries: list[dict], exclude_uuids: set[str]) Args: entries: List of history entries exclude_uuids: Set of UUIDs to exclude from calculation - + Returns: Tuple of (total_cost, total_tokens, call_count) """ @@ -248,7 +248,7 @@ class SignatureContext: def __init__(self, signature_name: str, tracker: "CostTracker") -> None: """Initialize the signature context. - + Args: signature_name: Name of the signature tracker: CostTracker instance @@ -337,4 +337,4 @@ async def __aexit__( def get_cost_tracker() -> CostTracker: """Get the global cost tracker instance.""" - return _cost_tracker \ No newline at end of file + return _cost_tracker diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index e3b33e6..82bdafd 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -3,12 +3,11 @@ import logging import dspy # type: ignore[import-untyped] -from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] +from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] from codespy.config import Settings, get_settings -from codespy.config_memory import LLMSettings, REFLECTION_MODULES - +from codespy.config_memory import REFLECTION_MODULES, LLMSettings logger = logging.getLogger(__name__) @@ -56,18 +55,14 @@ def _supports_cache_control(model: str) -> bool: """ try: info = litellm.get_model_info(model) - if info.get("cache_creation_input_token_cost") is not None: - return True - return False + return info.get("cache_creation_input_token_cost") is not None except Exception: # Model not in LiteLLM DB (Ollama offline, custom endpoint). # Fall back to prefix heuristic. lower = model.lower() if lower.startswith("anthropic/"): return True - if lower.startswith("bedrock/") and "anthropic" in lower: - return True - return False + return bool(lower.startswith("bedrock/") and "anthropic" in lower) def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: @@ -230,9 +225,9 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: """ # Collect all unique models from config models_to_check: set[str] = {settings.default_model} - + # Check all signature-specific models - for sig_name, sig_config in settings.signatures.items(): + for _sig_name, sig_config in settings.signatures.items(): if sig_config.model: models_to_check.add(sig_config.model) @@ -248,7 +243,7 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: # Check each model verified: list[str] = [] failed: list[str] = [] - + for model in models_to_check: try: litellm.completion( @@ -266,26 +261,24 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: failed.append(f"{model}: connection error - {e}") except Exception as e: failed.append(f"{model}: {e}") - + if failed: return False, f"Model verification failed: {'; '.join(failed)}" - + return True, f"Verified {len(verified)} model(s): {', '.join(verified)}" class _TaskDestroyedFilter(logging.Filter): """Filter to suppress 'Task was destroyed' messages from asyncio.""" - + def filter(self, record: logging.LogRecord) -> bool: msg = record.getMessage() - if "Task was destroyed" in msg and "LoggingWorker" in msg: - return False - return True + return not ("Task was destroyed" in msg and "LoggingWorker" in msg) class _MCPRequestFilter(logging.Filter): """Filter to suppress all noisy 'Processing request of type' MCP server messages.""" - + def filter(self, record: logging.LogRecord) -> bool: return "Processing request of type" not in record.getMessage() @@ -295,4 +288,4 @@ def filter(self, record: logging.LogRecord) -> bool: # Suppress noisy MCP server "Processing request" messages logging.getLogger("mcp.server").addFilter(_MCPRequestFilter()) -logging.getLogger("mcp.server.lowlevel").addFilter(_MCPRequestFilter()) \ No newline at end of file +logging.getLogger("mcp.server.lowlevel").addFilter(_MCPRequestFilter()) diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 542364e..402a2e7 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -3,7 +3,7 @@ import logging import os import uuid -from enum import Enum +from enum import StrEnum from typing import Literal from pydantic import BaseModel, Field @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) -class ItemTag(str, Enum): +class ItemTag(StrEnum): """How a context-memory item performed in the trajectory just observed. - helpful: directly aided orientation or answering; keep. @@ -26,7 +26,7 @@ class ItemTag(str, Enum): STALE = "stale" -class OpType(str, Enum): +class OpType(StrEnum): """Cartographer edit operations against the context memory.""" ADD = "ADD" @@ -339,7 +339,7 @@ def from_json(cls, text: str) -> ContextMemory: return cls.model_validate_json(text) @classmethod - def merge(cls, *memories: "ContextMemory") -> "ContextMemory": + def merge(cls, *memories: ContextMemory) -> ContextMemory: """Merge multiple context memories into a single memory. Later memories win on ID collision (items with duplicate IDs are diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 09ceb4c..d0f2ab3 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import UTC, datetime, timezone +from datetime import UTC, datetime from pydantic import BaseModel, Field @@ -172,7 +172,7 @@ def find_latest_episode( if not candidates: return None # Sort by modified_at descending; epoch fallback for entries without timestamp - _epoch = datetime.min.replace(tzinfo=timezone.utc) + _epoch = datetime.min.replace(tzinfo=UTC) candidates.sort( key=lambda e: e.modified_at if e.modified_at is not None else _epoch, reverse=True, diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index b4faf8f..bfdc4a9 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -8,7 +8,6 @@ import dspy - logger = logging.getLogger(__name__) from codespy.agents.memory.hippocampus.budget import ( @@ -19,7 +18,13 @@ format_inputs, format_trajectory, ) -from codespy.agents.memory.hippocampus.context_memory import ContextMemory, ItemTag, Mutation, Operation, OpType +from codespy.agents.memory.hippocampus.context_memory import ( + ContextMemory, + ItemTag, + Mutation, + Operation, + OpType, +) from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.hippocampus.episode import load_episode as _load_episode from codespy.agents.memory.hippocampus.episode import save_episode as _save_episode diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 96049d5..2db74a1 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -9,7 +9,6 @@ ) - class DistillerSig(dspy.Signature): """You are an expert analyst reviewing an agent's execution trajectory after it interacted with a long external context to answer a question. diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 0adec47..67e9dad 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -1,10 +1,10 @@ """Data models for code review results.""" from datetime import UTC, datetime -from enum import Enum +from enum import StrEnum from pathlib import Path -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field from codespy.agents.memory.hippocampus import ContextMemory @@ -23,7 +23,7 @@ class PRContext(BaseModel): summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") -class IssueSeverity(str, Enum): +class IssueSeverity(StrEnum): """Severity level of an issue.""" CRITICAL = "critical" @@ -33,7 +33,7 @@ class IssueSeverity(str, Enum): INFO = "info" -class IssueCategory(str, Enum): +class IssueCategory(StrEnum): """Category of an issue.""" SECURITY = "security" @@ -42,7 +42,7 @@ class IssueCategory(str, Enum): SMELL = "smell" -class ScopeType(str, Enum): +class ScopeType(StrEnum): """Type of code scope in a repository.""" LIBRARY = "library" # Shared code that others import @@ -144,7 +144,7 @@ def topic(self, repo_full_name: str) -> "Topic": Returns: Topic object with id and description """ - from codespy.agents.memory.hippocampus.context_memory import make_topic_id, Topic + from codespy.agents.memory.hippocampus.context_memory import Topic, make_topic_id package_name = self.package_manifest.package_name if self.package_manifest else None topic_id = make_topic_id(repo_full_name, self.subroot, package_name) diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 3d21e64..a0664b3 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -1,13 +1,14 @@ """Auditor module — assesses code quality and provides recommendation after reviews.""" import logging -from typing import TYPE_CHECKING, Sequence +from collections.abc import Sequence +from typing import TYPE_CHECKING import dspy from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus +from codespy.agents.memory.hippocampus import Hippocampus from codespy.agents.reviewer.models import Issue, ReviewContext from codespy.agents.reviewer.modules.scope_resolver import _deepest_common_folder from codespy.config import get_settings diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 74b778c..2776cee 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -2,8 +2,9 @@ import asyncio import logging +from collections.abc import Sequence from pathlib import Path -from typing import Any, Sequence +from typing import Any import dspy # type: ignore[import-untyped] @@ -11,7 +12,6 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult - from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, @@ -19,7 +19,6 @@ resolve_scope_root, restore_repo_paths, ) - from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 88d0084..36f313f 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -2,8 +2,7 @@ import asyncio import logging -from pathlib import Path -from typing import Sequence +from collections.abc import Sequence import dspy # type: ignore[import-untyped] @@ -11,7 +10,6 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult - from codespy.agents.reviewer.modules.doc_extractor import extract_documentation from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index 51e6e31..ade8f7c 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -4,11 +4,12 @@ import logging import os +from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING, Sequence +from typing import TYPE_CHECKING -from codespy.tools.git.models import ChangedFile from codespy.agents.reviewer.models import Issue +from codespy.tools.git.models import ChangedFile if TYPE_CHECKING: from codespy.agents.reviewer.models import ScopeResult @@ -58,10 +59,10 @@ def is_markdown_file(filename: str) -> bool: def get_language(file: ChangedFile) -> str: """Get the programming language for a file based on extension. - + Args: file: The changed file - + Returns: Language name or "Unknown" """ @@ -118,7 +119,8 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: New ScopeResult with scope-relative file paths in changed_files. The subroot is set to "." since paths are now relative to it. """ - from codespy.agents.reviewer.models import PackageManifest, ScopeResult as SR + from codespy.agents.reviewer.models import PackageManifest + from codespy.agents.reviewer.models import ScopeResult as SR if scope.subroot == ".": return scope # Already at repo root, no transformation needed diff --git a/src/codespy/agents/reviewer/modules/manifest_parser.py b/src/codespy/agents/reviewer/modules/manifest_parser.py index c36b904..921c912 100644 --- a/src/codespy/agents/reviewer/modules/manifest_parser.py +++ b/src/codespy/agents/reviewer/modules/manifest_parser.py @@ -131,7 +131,7 @@ def extract_dependencies(manifest_path: str, repo_path: Path) -> tuple[list[str] def _extract_deps_from_package_json(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from package.json (production only, skip dev/peer/optional).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: data = json.load(f) deps = data.get("dependencies", {}) @@ -161,7 +161,7 @@ def _extract_deps_from_package_json(path: Path) -> tuple[list[str], dict[str, st def _extract_deps_from_go_mod(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from go.mod (filter // indirect lines).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() names: list[str] = [] @@ -326,14 +326,14 @@ def _extract_deps_from_pom_xml(path: Path) -> tuple[list[str], dict[str, str]]: def _extract_deps_from_composer_json(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from composer.json (exclude php, ext-*).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: data = json.load(f) require = data.get("require", {}) if not isinstance(require, dict): return [], {} - names = [name for name in require.keys() + names = [name for name in require if not name.startswith("php") and not name.startswith("ext-")] return names, {} @@ -346,7 +346,7 @@ def _extract_deps_from_pubspec_yaml(path: Path) -> tuple[list[str], dict[str, st try: import yaml - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) deps = data.get("dependencies", {}) @@ -382,7 +382,7 @@ def _extract_deps_from_pubspec_yaml(path: Path) -> tuple[list[str], dict[str, st def _extract_deps_from_gemfile(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from Gemfile (skip dev/test groups).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() names: list[str] = [] @@ -437,7 +437,7 @@ def _extract_deps_from_gemfile(path: Path) -> tuple[list[str], dict[str, str]]: def _extract_deps_from_mix_exs(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from mix.exs (skip dev/test only).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() names: list[str] = [] @@ -511,7 +511,7 @@ def _extract_deps_from_csproj(path: Path) -> tuple[list[str], dict[str, str]]: def _extract_deps_from_swift_package(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from Package.swift.""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() names: list[str] = [] @@ -541,7 +541,7 @@ def _extract_deps_from_swift_package(path: Path) -> tuple[list[str], dict[str, s def _extract_deps_from_gradle(path: Path) -> tuple[list[str], dict[str, str]]: """Extract deps from build.gradle/build.gradle.kts (skip test/debug).""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() names: list[str] = [] @@ -603,9 +603,7 @@ def extract_package_name(manifest_path: str, repo_path: Path) -> str | None: filename = Path(manifest_path).name try: - if filename == "package.json": - return _extract_from_json(full_path, ["name"]) - elif filename == "composer.json": + if filename == "package.json" or filename == "composer.json": return _extract_from_json(full_path, ["name"]) elif filename == "go.mod": return _extract_from_go_mod(full_path) @@ -613,9 +611,7 @@ def extract_package_name(manifest_path: str, repo_path: Path) -> str | None: return _extract_from_pyproject_toml(full_path) elif filename == "Cargo.toml": return _extract_from_toml(full_path, ["package", "name"]) - elif filename == "pubspec.yaml": - return _extract_from_yaml(full_path, ["name"]) - elif filename == "Chart.yaml": + elif filename == "pubspec.yaml" or filename == "Chart.yaml": return _extract_from_yaml(full_path, ["name"]) elif filename == "pom.xml": return _extract_from_pom_xml(full_path) @@ -640,7 +636,7 @@ def extract_package_name(manifest_path: str, repo_path: Path) -> str | None: def _extract_from_json(path: Path, keys: list[str]) -> str | None: """Extract value from JSON file following key path.""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: data = json.load(f) value = data for key in keys: @@ -657,7 +653,7 @@ def _extract_from_json(path: Path, keys: list[str]) -> str | None: def _extract_from_go_mod(path: Path) -> str | None: """Extract module name from go.mod file.""" try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: first_line = f.readline().strip() match = re.match(r"^module\s+(\S+)", first_line) return match.group(1) if match else None @@ -721,7 +717,7 @@ def _extract_from_yaml(path: Path, keys: list[str]) -> str | None: try: import yaml - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) value = data for key in keys: @@ -820,7 +816,7 @@ def _extract_from_gradle(repo_path: Path, manifest_path: str) -> str | None: settings_path = repo_path / manifest_dir / settings_file if settings_path.exists(): try: - with open(settings_path, "r", encoding="utf-8") as f: + with open(settings_path, encoding="utf-8") as f: content = f.read() # Look for rootProject.name = 'name' or rootProject.name = "name" match = re.search( @@ -852,7 +848,7 @@ def _extract_from_swift_package(path: Path) -> str | None: Looks for 'name: "..."' in the Package initialization. """ try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() # Look for Package(name: "...") match = re.search( @@ -873,7 +869,7 @@ def _extract_from_mix_exs(path: Path) -> str | None: Looks for 'def project do' and extracts the 'app:' value. """ try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: content = f.read() # Look for app: :name or app: "name" match = re.search(r"app:\s*[:\"]([^\"\s,)]+)[\"\s,)]", content) diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index df3ceac..ad0fbd8 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -22,23 +22,22 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import ( - PRContext, PackageManifest, ReviewContext, ScopeResult, ScopeType, ) +from codespy.agents.reviewer.modules.manifest_parser import ( + PACKAGE_MANAGER_TO_ECOSYSTEM, + extract_dependencies, + extract_package_name, + infer_repo_from_name, +) from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.client import get_client from codespy.tools.git.models import ChangedFile, PullRequest, should_review_file from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server -from codespy.agents.reviewer.modules.manifest_parser import ( - extract_package_name, - extract_dependencies, - PACKAGE_MANAGER_TO_ECOSYSTEM, - infer_repo_from_name, -) logger = logging.getLogger(__name__) @@ -908,7 +907,7 @@ async def _refine_scopes( scopes: list[ScopeResult], orphans: list[ChangedFile], review_context: ReviewContext, - ) -> tuple[list[ScopeResult], "ContextMemory | None"]: + ) -> tuple[list[ScopeResult], ContextMemory | None]: """Use ReAct agent to refine scope assignments from deterministic candidates. Args: @@ -921,7 +920,10 @@ async def _refine_scopes( ContextMemory with topics and items from Hippocampus) """ from codespy.agents.memory.hippocampus import ( - ContextMemory, Topic, compute_common_ancestor_topic_id, make_topic_id, + ContextMemory, + Topic, + compute_common_ancestor_topic_id, + make_topic_id, ) # Local bindings from review_context metadata @@ -1102,7 +1104,7 @@ async def _refine_scopes( async def aforward( self, review_context: ReviewContext, - ) -> tuple[list[ScopeResult], "ContextMemory | None"]: + ) -> tuple[list[ScopeResult], ContextMemory | None]: """Resolve scopes in the repository for the given PR. Args: diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index 498afc2..a01cf94 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -2,8 +2,9 @@ import asyncio import logging +from collections.abc import Sequence from pathlib import Path -from typing import Any, Sequence +from typing import Any import dspy # type: ignore[import-untyped] @@ -11,7 +12,6 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult - from codespy.agents.reviewer.modules.helpers import ( MIN_CONFIDENCE, issues_to_markdown, diff --git a/src/codespy/agents/reviewer/reporters/__init__.py b/src/codespy/agents/reviewer/reporters/__init__.py index 4db9ad5..f1eaf7b 100644 --- a/src/codespy/agents/reviewer/reporters/__init__.py +++ b/src/codespy/agents/reviewer/reporters/__init__.py @@ -8,4 +8,4 @@ "BaseReporter", "StdoutReporter", "GitReporter", -] \ No newline at end of file +] diff --git a/src/codespy/agents/reviewer/reporters/base.py b/src/codespy/agents/reviewer/reporters/base.py index 894ee36..0d3e449 100644 --- a/src/codespy/agents/reviewer/reporters/base.py +++ b/src/codespy/agents/reviewer/reporters/base.py @@ -15,4 +15,4 @@ def report(self, result: ReviewResult) -> None: Args: result: The review result to report. """ - pass \ No newline at end of file + pass diff --git a/src/codespy/agents/reviewer/reporters/git.py b/src/codespy/agents/reviewer/reporters/git.py index 21699b5..fb3a1a6 100644 --- a/src/codespy/agents/reviewer/reporters/git.py +++ b/src/codespy/agents/reviewer/reporters/git.py @@ -305,4 +305,4 @@ def _build_inline_comments(self, issues: list[Issue]) -> list[dict]: # Backward compatibility alias -GitHubPRReporter = GitReporter \ No newline at end of file +GitHubPRReporter = GitReporter diff --git a/src/codespy/agents/reviewer/reporters/stdout.py b/src/codespy/agents/reviewer/reporters/stdout.py index eba403b..e766512 100644 --- a/src/codespy/agents/reviewer/reporters/stdout.py +++ b/src/codespy/agents/reviewer/reporters/stdout.py @@ -35,4 +35,4 @@ def report(self, result: ReviewResult) -> None: if self.format == "json": self.console.print_json(json.dumps(result.to_json_dict(), indent=2)) else: - self.console.print(result.to_markdown()) \ No newline at end of file + self.console.print(result.to_markdown()) diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index be3c650..c6ba5e0 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -4,27 +4,21 @@ import logging import uuid from pathlib import Path -from typing import Sequence import dspy # type: ignore[import-untyped] from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access -from codespy.config import Settings, get_settings -from codespy.config_memory import verify_memory_access -from codespy.tools.git import GitClient, get_client, ChangedFile, PullRequest -from codespy.tools.git.local_diff import build_pr_from_diff -from codespy.tools.git.patch_utils import compact_patches from codespy.agents.memory.hippocampus import ContextMemory from codespy.agents.reviewer.models import ( Issue, + LocalReviewConfig, PRContext, + RemoteReviewConfig, + ReviewConfig, ReviewContext, ReviewMetadata, - SignatureStatsResult, ReviewResult, - ReviewConfig, - RemoteReviewConfig, - LocalReviewConfig, + SignatureStatsResult, ) from codespy.agents.reviewer.modules import ( Auditor, @@ -36,6 +30,11 @@ ) from codespy.agents.reviewer.modules.helpers import build_patches from codespy.agents.reviewer.modules.scope_resolver import MANIFEST_FILES, MANIFEST_GLOBS +from codespy.config import Settings, get_settings +from codespy.config_memory import verify_memory_access +from codespy.tools.git import ChangedFile, GitClient, PullRequest, get_client +from codespy.tools.git.local_diff import build_pr_from_diff +from codespy.tools.git.patch_utils import compact_patches logger = logging.getLogger(__name__) @@ -137,10 +136,10 @@ async def _run_review_modules( def _build_local_pr(self, config: LocalReviewConfig) -> PullRequest: """Build a PullRequest from local git changes. - + Args: config: Local review configuration - + Returns: PullRequest object built from local git changes """ @@ -207,7 +206,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: if manifest.lock_file_path: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: - logger.info(f" Dependencies changed: Yes") + logger.info(" Dependencies changed: Yes") # Expand sparse checkout to cover full scope subtrees if not is_local: self._expand_sparse_for_scopes(scopes, repo_path) diff --git a/src/codespy/cli.py b/src/codespy/cli.py index e8f68a9..e4f09bb 100644 --- a/src/codespy/cli.py +++ b/src/codespy/cli.py @@ -7,13 +7,13 @@ from rich.panel import Panel from codespy import __version__ -from codespy.config import get_settings -from codespy.config_git import get_github_token_source, get_gitlab_token_source +from codespy.cli_local import review_local, review_uncommitted +from codespy.cli_mcp_server import serve # Import command functions from submodules from codespy.cli_remote import review -from codespy.cli_local import review_local, review_uncommitted -from codespy.cli_mcp_server import serve +from codespy.config import get_settings +from codespy.config_git import get_github_token_source, get_gitlab_token_source app = typer.Typer( name="codespy", diff --git a/src/codespy/cli_local.py b/src/codespy/cli_local.py index 649f710..e479ced 100644 --- a/src/codespy/cli_local.py +++ b/src/codespy/cli_local.py @@ -86,11 +86,11 @@ def review_local( settings.output_format = output # type: ignore repo = Path(repo_path if repo_path else os.getcwd()).resolve() - + if not repo.exists(): console.print(f"[red]Error:[/red] Directory does not exist: {repo}") raise typer.Exit(1) - + if not (repo / ".git").exists(): console.print(f"[red]Error:[/red] Not a git repository: {repo}") raise typer.Exit(1) @@ -107,8 +107,8 @@ def review_local( ) try: - from codespy.agents.reviewer.reviewer import ReviewPipeline from codespy.agents.reviewer.models import LocalReviewConfig + from codespy.agents.reviewer.reviewer import ReviewPipeline pipeline = ReviewPipeline(settings) @@ -118,7 +118,7 @@ def review_local( base_ref=base_ref, uncommitted=False ) - + # Run review (model access always verified in pipeline) result = pipeline(config) @@ -203,11 +203,11 @@ def review_uncommitted( settings.output_format = output # type: ignore repo = Path(repo_path if repo_path else os.getcwd()).resolve() - + if not repo.exists(): console.print(f"[red]Error:[/red] Directory does not exist: {repo}") raise typer.Exit(1) - + if not (repo / ".git").exists(): console.print(f"[red]Error:[/red] Not a git repository: {repo}") raise typer.Exit(1) @@ -223,8 +223,8 @@ def review_uncommitted( ) try: - from codespy.agents.reviewer.reviewer import ReviewPipeline from codespy.agents.reviewer.models import LocalReviewConfig + from codespy.agents.reviewer.reviewer import ReviewPipeline pipeline = ReviewPipeline(settings) @@ -233,7 +233,7 @@ def review_uncommitted( repo_path=repo, uncommitted=True ) - + # Run review (model access always verified in pipeline) result = pipeline(config) diff --git a/src/codespy/cli_remote.py b/src/codespy/cli_remote.py index c549aeb..7610e2e 100644 --- a/src/codespy/cli_remote.py +++ b/src/codespy/cli_remote.py @@ -87,7 +87,7 @@ def review( # Print config at startup (secrets are hidden via repr=False) logging.info(f"Loaded config: {settings}") - + # Log module configurations settings.log_signature_configs() @@ -113,7 +113,7 @@ def review( # Detect platform and validate token platform = detect_platform(pr_url) - + if platform == "github": token = settings.github_token token_source = get_github_token_source() @@ -156,14 +156,14 @@ def review( ) try: - from codespy.agents.reviewer.reviewer import ReviewPipeline from codespy.agents.reviewer.models import RemoteReviewConfig + from codespy.agents.reviewer.reviewer import ReviewPipeline pipeline = ReviewPipeline(settings) # Create remote review config config = RemoteReviewConfig(url=pr_url) - + # Run review (model access always verified in pipeline) result = pipeline(config) diff --git a/src/codespy/config.py b/src/codespy/config.py index f2bb9da..cc28b1e 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import TYPE_CHECKING, Any - import yaml from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -14,7 +13,6 @@ SignatureConfig, apply_signature_env_overrides, ) - from codespy.config_git import ( GitHubConfig, GitLabConfig, @@ -34,9 +32,9 @@ discover_openai_api_key, ) from codespy.config_memory import ( + REFLECTION_MODULES, LLMSettings, MemoryConfig, - REFLECTION_MODULES, apply_memory_env_overrides, reset_memory_store, ) diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index db61b37..0b3fdca 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -4,7 +4,6 @@ import os from typing import Any, Literal - from pydantic import BaseModel, Field logger = logging.getLogger(__name__) diff --git a/src/codespy/config_git.py b/src/codespy/config_git.py index 6a9b556..6aa7563 100644 --- a/src/codespy/config_git.py +++ b/src/codespy/config_git.py @@ -195,4 +195,4 @@ class GitLabConfig(BaseModel): token: str | None = Field(default=None, repr=False) url: str = "https://gitlab.com" # Can be changed for self-hosted instances - auto_discover_token: bool = True \ No newline at end of file + auto_discover_token: bool = True diff --git a/src/codespy/config_io.py b/src/codespy/config_io.py index 03606e2..d3890c5 100644 --- a/src/codespy/config_io.py +++ b/src/codespy/config_io.py @@ -37,4 +37,4 @@ ".pytest_cache", ".mypy_cache", ".ruff_cache", -] \ No newline at end of file +] diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 9aa7739..8cd2d46 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -10,7 +10,6 @@ from codespy.config_dspy import ReasoningEffort from codespy.tools.storage.base import Storage - if TYPE_CHECKING: from codespy.config import Settings diff --git a/src/codespy/tools/__init__.py b/src/codespy/tools/__init__.py index f276315..d5a6511 100644 --- a/src/codespy/tools/__init__.py +++ b/src/codespy/tools/__init__.py @@ -1,7 +1,6 @@ """Tools for code parsing, Git platform integration, filesystem operations, web browsing, and security scanning.""" from codespy.tools.cyber import OSVClient, ScanResult, ScanSummary, Vulnerability -from codespy.tools.storage import FileSystem, S3Client, Storage from codespy.tools.git import ( ChangedFile, GitClient, @@ -10,6 +9,7 @@ get_client, ) from codespy.tools.parsers import RipgrepSearch, SearchResult, TreeSitterParser +from codespy.tools.storage import FileSystem, S3Client, Storage from codespy.tools.web import SearchResults, WebBrowser, WebPage # Note: GitReporter is not exported here to avoid circular imports. diff --git a/src/codespy/tools/cyber/__init__.py b/src/codespy/tools/cyber/__init__.py index a16b662..443f2a0 100644 --- a/src/codespy/tools/cyber/__init__.py +++ b/src/codespy/tools/cyber/__init__.py @@ -14,4 +14,4 @@ "ScanSummary", "Vulnerability", "osv_mcp", -] \ No newline at end of file +] diff --git a/src/codespy/tools/cyber/osv/__init__.py b/src/codespy/tools/cyber/osv/__init__.py index 8c3c9d5..9643221 100644 --- a/src/codespy/tools/cyber/osv/__init__.py +++ b/src/codespy/tools/cyber/osv/__init__.py @@ -1,7 +1,6 @@ """OSV (Open Source Vulnerabilities) API integration for codespy.""" from codespy.tools.cyber.osv.client import OSVClient -from codespy.tools.cyber.osv.server import mcp as osv_mcp from codespy.tools.cyber.osv.models import ( AffectedPackage, BatchQueryResponse, @@ -22,6 +21,7 @@ VulnerabilityQuery, VulnerabilityResponse, ) +from codespy.tools.cyber.osv.server import mcp as osv_mcp __all__ = [ # Client @@ -51,4 +51,4 @@ "Ecosystem", "SeverityType", "ReferenceType", -] \ No newline at end of file +] diff --git a/src/codespy/tools/cyber/osv/client.py b/src/codespy/tools/cyber/osv/client.py index eed3791..a8d560d 100644 --- a/src/codespy/tools/cyber/osv/client.py +++ b/src/codespy/tools/cyber/osv/client.py @@ -7,7 +7,6 @@ from codespy.tools.cyber.osv.models import ( BatchQueryResponse, - BatchQueryResult, PackageQuery, ScanResult, ScanSummary, @@ -336,7 +335,7 @@ def scan_dependencies( try: batch_response = self.query_batch(dependencies) - for i, (dep, query_result) in enumerate( + for _i, (dep, query_result) in enumerate( zip(dependencies, batch_response.results, strict=False) ): result = ScanResult( @@ -448,4 +447,4 @@ def scan_cargo_package(self, name: str, version: str) -> ScanResult: Returns: ScanResult with vulnerabilities found """ - return self.scan_package(name, "crates.io", version) \ No newline at end of file + return self.scan_package(name, "crates.io", version) diff --git a/src/codespy/tools/cyber/osv/models.py b/src/codespy/tools/cyber/osv/models.py index 5c8c1a9..5abb312 100644 --- a/src/codespy/tools/cyber/osv/models.py +++ b/src/codespy/tools/cyber/osv/models.py @@ -1,12 +1,12 @@ """Models for OSV (Open Source Vulnerabilities) API.""" from datetime import datetime -from enum import Enum +from enum import StrEnum from pydantic import BaseModel, Field -class Ecosystem(str, Enum): +class Ecosystem(StrEnum): """Supported package ecosystems in OSV.""" GO = "Go" @@ -38,7 +38,7 @@ class Ecosystem(str, Enum): WOLFI = "Wolfi" -class SeverityType(str, Enum): +class SeverityType(StrEnum): """Severity scoring systems.""" CVSS_V2 = "CVSS_V2" @@ -46,7 +46,7 @@ class SeverityType(str, Enum): CVSS_V4 = "CVSS_V4" -class ReferenceType(str, Enum): +class ReferenceType(StrEnum): """Types of references in vulnerability records.""" ADVISORY = "ADVISORY" @@ -332,4 +332,4 @@ def to_markdown(self) -> str: lines.append(result.to_markdown()) lines.append("\n---\n") - return "\n".join(lines) \ No newline at end of file + return "\n".join(lines) diff --git a/src/codespy/tools/cyber/osv/server.py b/src/codespy/tools/cyber/osv/server.py index db212f9..f37dd4c 100644 --- a/src/codespy/tools/cyber/osv/server.py +++ b/src/codespy/tools/cyber/osv/server.py @@ -228,6 +228,6 @@ def scan_cargo_package(name: str, version: str) -> dict[str, Any]: # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + _client = OSVClient() mcp.run() diff --git a/src/codespy/tools/git/base.py b/src/codespy/tools/git/base.py index bee9560..b2350e9 100644 --- a/src/codespy/tools/git/base.py +++ b/src/codespy/tools/git/base.py @@ -112,4 +112,4 @@ def can_handle(url: str) -> bool: Returns: True if this client can handle the URL """ - ... \ No newline at end of file + ... diff --git a/src/codespy/tools/git/client.py b/src/codespy/tools/git/client.py index 01d93b8..d90b6ec 100644 --- a/src/codespy/tools/git/client.py +++ b/src/codespy/tools/git/client.py @@ -77,4 +77,4 @@ def is_supported_url(url: str) -> bool: Returns: True if URL is supported, False otherwise """ - return any(client_class.can_handle(url) for client_class in _CLIENT_CLASSES) \ No newline at end of file + return any(client_class.can_handle(url) for client_class in _CLIENT_CLASSES) diff --git a/src/codespy/tools/git/models.py b/src/codespy/tools/git/models.py index e521adb..79cec59 100644 --- a/src/codespy/tools/git/models.py +++ b/src/codespy/tools/git/models.py @@ -2,12 +2,12 @@ import re from datetime import datetime -from enum import Enum +from enum import StrEnum from pydantic import BaseModel, Field -class FileStatus(str, Enum): +class FileStatus(StrEnum): """Status of a file in a pull request.""" ADDED = "added" @@ -16,7 +16,7 @@ class FileStatus(str, Enum): RENAMED = "renamed" -class GitPlatform(str, Enum): +class GitPlatform(StrEnum): """Supported Git platforms.""" GITHUB = "github" @@ -135,7 +135,7 @@ def is_source_map(self) -> bool: def is_in_excluded_directory(self, excluded_directories: list[str]) -> bool: """Check if this file is in an excluded directory. - + Args: excluded_directories: List of directory names to exclude (from settings) """ @@ -146,11 +146,11 @@ def is_in_excluded_directory(self, excluded_directories: list[str]) -> bool: @property def valid_new_line_numbers(self) -> set[int]: """Get line numbers in the new file that are valid for inline comments. - + Parses the unified diff patch to extract line numbers where inline comments can be placed. Only lines that appear in the diff (additions and context lines) are valid for GitHub/GitLab review comments. - + Returns: Set of valid line numbers in the new version of the file """ @@ -172,11 +172,7 @@ def valid_new_line_numbers(self) -> set[int]: continue # Context line (unchanged) - valid for comments - if line.startswith(" "): - valid_lines.add(current_new_line) - current_new_line += 1 - # Addition line - valid for comments - elif line.startswith("+"): + if line.startswith(" ") or line.startswith("+"): valid_lines.add(current_new_line) current_new_line += 1 # Deletion line - doesn't increment new line counter (not in new file) @@ -190,10 +186,10 @@ def valid_new_line_numbers(self) -> set[int]: def is_line_in_diff(self, line_number: int) -> bool: """Check if a line number is valid for inline comments. - + Args: line_number: Line number to check - + Returns: True if the line is part of the diff and can receive inline comments """ @@ -202,11 +198,11 @@ def is_line_in_diff(self, line_number: int) -> bool: def should_review_file(file: ChangedFile, excluded_directories: list[str]) -> bool: """Check if a file should be included in code review. - + Args: file: The ChangedFile to check excluded_directories: List of directory names to exclude (from settings) - + Returns: True if file should be reviewed, False if it should be skipped """ @@ -218,9 +214,7 @@ def should_review_file(file: ChangedFile, excluded_directories: list[str]) -> bo return False if file.is_source_map: return False - if file.is_in_excluded_directory(excluded_directories): - return False - return True + return not file.is_in_excluded_directory(excluded_directories) class PullRequest(BaseModel): diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py index b94f24e..fd16504 100644 --- a/src/codespy/tools/git/patch_utils.py +++ b/src/codespy/tools/git/patch_utils.py @@ -93,10 +93,7 @@ def _should_compact_file(file: ChangedFile) -> bool: return False # Skip binary and lock files - if file.is_binary or file.is_lock_file: - return False - - return True + return not (file.is_binary or file.is_lock_file) def compact_patch( @@ -278,12 +275,11 @@ def _expand_hunk_to_functions( # Find innermost enclosing function best_match: FunctionInfo | None = None for func in functions: - if func.line_start <= line_num <= func.line_end: - if best_match is None or ( - func.line_start >= best_match.line_start - and func.line_end <= best_match.line_end - ): - best_match = func + if func.line_start <= line_num <= func.line_end and (best_match is None or ( + func.line_start >= best_match.line_start + and func.line_end <= best_match.line_end + )): + best_match = func if best_match and best_match not in enclosing_functions: enclosing_functions.append(best_match) diff --git a/src/codespy/tools/git/server.py b/src/codespy/tools/git/server.py index 4778f93..7379780 100644 --- a/src/codespy/tools/git/server.py +++ b/src/codespy/tools/git/server.py @@ -2,13 +2,12 @@ import logging import os -import sys from mcp.server.fastmcp import FastMCP from codespy.config import Settings -from codespy.tools.git.client import get_client, detect_platform, is_supported_url from codespy.tools.git.base import GitClient +from codespy.tools.git.client import detect_platform, get_client, is_supported_url logger = logging.getLogger(__name__) @@ -102,11 +101,11 @@ def clone_repository( Path to the cloned repository """ from pathlib import Path - from codespy.tools.git.models import GitPlatform - + + if _settings is None: raise RuntimeError("Settings not initialized") - + # Build a URL to get the right client if platform.lower() == "gitlab": # Use the configured GitLab URL or default @@ -114,7 +113,7 @@ def clone_repository( dummy_url = f"{gitlab_base}/{owner}/{repo_name}/-/merge_requests/1" else: dummy_url = f"https://github.com/{owner}/{repo_name}/pull/1" - + client = _get_client(dummy_url) logger.info(f"[GIT] {_caller_module} -> clone_repository: {owner}/{repo_name}@{ref[:8]} ({platform})") path = client.clone_repository( @@ -155,7 +154,7 @@ def detect_git_platform(url: str) -> dict: # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + # Initialize with settings from environment _settings = Settings() - mcp.run() \ No newline at end of file + mcp.run() diff --git a/src/codespy/tools/mcp_utils.py b/src/codespy/tools/mcp_utils.py index 9d81573..e075d4d 100644 --- a/src/codespy/tools/mcp_utils.py +++ b/src/codespy/tools/mcp_utils.py @@ -53,7 +53,7 @@ async def connect_mcp_server( await session.__aenter__() contexts.append(session) await session.initialize() - + tools_response = await session.list_tools() return [dspy.Tool.from_mcp_tool(session, tool) for tool in tools_response.tools] @@ -72,4 +72,4 @@ async def cleanup_mcp_contexts(contexts: list[Any]) -> None: try: await ctx.__aexit__(None, None, None) except Exception as e: - logger.warning(f"Error cleaning up MCP context: {e}") \ No newline at end of file + logger.warning(f"Error cleaning up MCP context: {e}") diff --git a/src/codespy/tools/parsers/ripgrep/__init__.py b/src/codespy/tools/parsers/ripgrep/__init__.py index be52fa7..b4b915a 100644 --- a/src/codespy/tools/parsers/ripgrep/__init__.py +++ b/src/codespy/tools/parsers/ripgrep/__init__.py @@ -2,4 +2,4 @@ from codespy.tools.parsers.ripgrep.client import RipgrepSearch, SearchResult -__all__ = ["RipgrepSearch", "SearchResult"] \ No newline at end of file +__all__ = ["RipgrepSearch", "SearchResult"] diff --git a/src/codespy/tools/parsers/ripgrep/server.py b/src/codespy/tools/parsers/ripgrep/server.py index 1d6ea3d..a7a53ed 100644 --- a/src/codespy/tools/parsers/ripgrep/server.py +++ b/src/codespy/tools/parsers/ripgrep/server.py @@ -187,7 +187,7 @@ def search_literal( # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + repo_path = sys.argv[1] if len(sys.argv) > 1 else "." _search = RipgrepSearch(repo_path) mcp.run() diff --git a/src/codespy/tools/parsers/treesitter/__init__.py b/src/codespy/tools/parsers/treesitter/__init__.py index 5c0d3e0..01a4c56 100644 --- a/src/codespy/tools/parsers/treesitter/__init__.py +++ b/src/codespy/tools/parsers/treesitter/__init__.py @@ -12,4 +12,4 @@ "FunctionInfo", "CallInfo", "SymbolInfo", -] \ No newline at end of file +] diff --git a/src/codespy/tools/parsers/treesitter/base_extractor.py b/src/codespy/tools/parsers/treesitter/base_extractor.py index 0fab5f0..5605e00 100644 --- a/src/codespy/tools/parsers/treesitter/base_extractor.py +++ b/src/codespy/tools/parsers/treesitter/base_extractor.py @@ -9,7 +9,7 @@ from codespy.tools.parsers.treesitter.models import FunctionInfo if TYPE_CHECKING: - from tree_sitter import Node + pass class BaseExtractor(ABC): @@ -64,4 +64,4 @@ def _get_node_text(self, node: Any, source: bytes) -> str: Returns: Decoded text content """ - return source[node.start_byte:node.end_byte].decode() \ No newline at end of file + return source[node.start_byte:node.end_byte].decode() diff --git a/src/codespy/tools/parsers/treesitter/extractors/cpp.py b/src/codespy/tools/parsers/treesitter/extractors/cpp.py index e81bac7..0924c6a 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/cpp.py +++ b/src/codespy/tools/parsers/treesitter/extractors/cpp.py @@ -53,9 +53,7 @@ def _extract_function_info( # Get name from the declarator name_part = declarator.child_by_field_name("declarator") if name_part: - if name_part.type == "identifier": - name_node = name_part - elif name_part.type == "field_identifier": + if name_part.type == "identifier" or name_part.type == "field_identifier": name_node = name_part elif name_part.type == "qualified_identifier": # C++ class method: Class::method diff --git a/src/codespy/tools/parsers/treesitter/extractors/go.py b/src/codespy/tools/parsers/treesitter/extractors/go.py index c21402d..462d8df 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/go.py +++ b/src/codespy/tools/parsers/treesitter/extractors/go.py @@ -83,4 +83,4 @@ def _extract_go_return_type(self, node: Any, source: bytes) -> str | None: result = node.child_by_field_name("result") if result: return self._get_node_text(result, source).strip() - return None \ No newline at end of file + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/java.py b/src/codespy/tools/parsers/treesitter/extractors/java.py index f32feff..45fd79a 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/java.py +++ b/src/codespy/tools/parsers/treesitter/extractors/java.py @@ -70,4 +70,4 @@ def _extract_java_return_type(self, node: Any, source: bytes) -> str | None: type_node = node.child_by_field_name("type") if type_node: return self._get_node_text(type_node, source).strip() - return None \ No newline at end of file + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/javascript.py b/src/codespy/tools/parsers/treesitter/extractors/javascript.py index 0069a0c..ade578a 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/javascript.py +++ b/src/codespy/tools/parsers/treesitter/extractors/javascript.py @@ -92,4 +92,4 @@ def _extract_js_params(self, node: Any, source: bytes) -> list[str]: if child.type in ("identifier", "required_parameter", "optional_parameter"): param_text = self._get_node_text(child, source) params.append(param_text.strip()) - return params \ No newline at end of file + return params diff --git a/src/codespy/tools/parsers/treesitter/extractors/kotlin.py b/src/codespy/tools/parsers/treesitter/extractors/kotlin.py index 006689c..db2ac7e 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +++ b/src/codespy/tools/parsers/treesitter/extractors/kotlin.py @@ -46,4 +46,4 @@ def visit(n: Any, in_class: bool = False) -> None: visit(child, in_class) visit(node) - return functions \ No newline at end of file + return functions diff --git a/src/codespy/tools/parsers/treesitter/extractors/objc.py b/src/codespy/tools/parsers/treesitter/extractors/objc.py index 47f2c35..339e615 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/objc.py +++ b/src/codespy/tools/parsers/treesitter/extractors/objc.py @@ -78,4 +78,4 @@ def _extract_c_declarator_name(self, node: Any, source: bytes) -> str | None: result = self._extract_c_declarator_name(child, source) if result: return result - return None \ No newline at end of file + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/python.py b/src/codespy/tools/parsers/treesitter/extractors/python.py index 96769bd..ef82a6c 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/python.py +++ b/src/codespy/tools/parsers/treesitter/extractors/python.py @@ -66,4 +66,4 @@ def _extract_python_return_type(self, node: Any, source: bytes) -> str | None: return_type = node.child_by_field_name("return_type") if return_type: return self._get_node_text(return_type, source).strip() - return None \ No newline at end of file + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py index dd245f8..5d2d6c6 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +++ b/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py @@ -8,10 +8,10 @@ Algorithm: 1. Run ripgrep on the file with generic definition patterns → sorted list of (line_number, function_name) - + 2. Derive implicit boundaries: each function spans from its definition line to the line before the next definition (or EOF) - + 3. Intersect these boundaries with changed_line_ranges → return functions that contain at least one changed line @@ -29,7 +29,6 @@ import shutil import subprocess from pathlib import Path -from typing import Any from codespy.tools.parsers.treesitter.models import FunctionInfo @@ -222,7 +221,7 @@ def _find_definitions( content = parts[2] # Try each pattern to extract function name - for pattern_name, pattern in patterns: + for _pattern_name, pattern in patterns: match = pattern.match(content) if match: func_name = match.group(1) if match.lastindex else None @@ -266,10 +265,7 @@ def _derive_boundaries( boundaries = [] for i, (line_num, func_name, full_line) in enumerate(definitions): # End is line before next definition, or EOF - if i + 1 < len(definitions): - end_line = definitions[i + 1][0] - 1 - else: - end_line = total_lines + end_line = definitions[i + 1][0] - 1 if i + 1 < len(definitions) else total_lines boundaries.append((line_num, end_line, func_name, full_line)) diff --git a/src/codespy/tools/parsers/treesitter/extractors/rust.py b/src/codespy/tools/parsers/treesitter/extractors/rust.py index 44676c7..095cd85 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/rust.py +++ b/src/codespy/tools/parsers/treesitter/extractors/rust.py @@ -56,10 +56,7 @@ def _extract_rust_params(self, node: Any, source: bytes) -> list[str]: params_node = node.child_by_field_name("parameters") if params_node: for child in params_node.children: - if child.type == "parameter": - param_text = self._get_node_text(child, source) - params.append(param_text.strip()) - elif child.type == "self_parameter": + if child.type == "parameter" or child.type == "self_parameter": param_text = self._get_node_text(child, source) params.append(param_text.strip()) return params @@ -69,4 +66,4 @@ def _extract_rust_return_type(self, node: Any, source: bytes) -> str | None: return_type = node.child_by_field_name("return_type") if return_type: return self._get_node_text(return_type, source).strip() - return None \ No newline at end of file + return None diff --git a/src/codespy/tools/parsers/treesitter/extractors/swift.py b/src/codespy/tools/parsers/treesitter/extractors/swift.py index a66b909..0818279 100644 --- a/src/codespy/tools/parsers/treesitter/extractors/swift.py +++ b/src/codespy/tools/parsers/treesitter/extractors/swift.py @@ -46,4 +46,4 @@ def visit(n: Any, in_class: bool = False) -> None: visit(child, in_class) visit(node) - return functions \ No newline at end of file + return functions diff --git a/src/codespy/tools/parsers/treesitter/server.py b/src/codespy/tools/parsers/treesitter/server.py index 0b4c8ee..eeb5617 100644 --- a/src/codespy/tools/parsers/treesitter/server.py +++ b/src/codespy/tools/parsers/treesitter/server.py @@ -341,7 +341,7 @@ def get_terraform_summary(file_path: str, content: str | None = None) -> dict[st # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + repo_path = sys.argv[1] if len(sys.argv) > 1 else "." _parser = TreeSitterParser(Path(repo_path)) mcp.run() diff --git a/src/codespy/tools/storage/base.py b/src/codespy/tools/storage/base.py index b46f327..f3f5f4d 100644 --- a/src/codespy/tools/storage/base.py +++ b/src/codespy/tools/storage/base.py @@ -6,7 +6,6 @@ from codespy.tools.storage.models import ( Content, - Entry, Info, Listing, OperationResult, diff --git a/src/codespy/tools/storage/filesystem/client.py b/src/codespy/tools/storage/filesystem/client.py index 3c0797f..e364f5e 100644 --- a/src/codespy/tools/storage/filesystem/client.py +++ b/src/codespy/tools/storage/filesystem/client.py @@ -4,7 +4,7 @@ import logging import os -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from codespy.tools.storage.base import Storage @@ -151,7 +151,7 @@ def list_directory( stat = entry.stat() size = stat.st_size if entry_type == EntryType.FILE else 0 - modified_at = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc) if entry_type == EntryType.FILE else None + modified_at = datetime.fromtimestamp(stat.st_mtime, tz=UTC) if entry_type == EntryType.FILE else None entries.append(Entry(name=entry.name, entry_type=entry_type, size=size, modified_at=modified_at)) except PermissionError as e: diff --git a/src/codespy/tools/storage/filesystem/server.py b/src/codespy/tools/storage/filesystem/server.py index 60b5f75..295cda4 100644 --- a/src/codespy/tools/storage/filesystem/server.py +++ b/src/codespy/tools/storage/filesystem/server.py @@ -5,7 +5,6 @@ import sys from collections import OrderedDict from functools import lru_cache -from pathlib import Path from mcp.server.fastmcp import FastMCP diff --git a/src/codespy/tools/storage/models.py b/src/codespy/tools/storage/models.py index 7704e8b..01e41ba 100644 --- a/src/codespy/tools/storage/models.py +++ b/src/codespy/tools/storage/models.py @@ -3,13 +3,13 @@ from __future__ import annotations from datetime import datetime -from enum import Enum +from enum import StrEnum from pathlib import Path from pydantic import BaseModel, Field -class EntryType(str, Enum): +class EntryType(StrEnum): """Type of storage entry.""" FILE = "file" @@ -84,7 +84,7 @@ class TreeNode(BaseModel): name: str = Field(description="Entry name") entry_type: EntryType = Field(description="Type of entry") - children: list["TreeNode"] = Field(default_factory=list, description="Child nodes") + children: list[TreeNode] = Field(default_factory=list, description="Child nodes") def to_string(self, prefix: str = "", is_last: bool = True) -> str: """Convert tree node to string representation. diff --git a/src/codespy/tools/web/__init__.py b/src/codespy/tools/web/__init__.py index 9443bc6..9207777 100644 --- a/src/codespy/tools/web/__init__.py +++ b/src/codespy/tools/web/__init__.py @@ -8,4 +8,4 @@ "WebPage", "SearchResult", "SearchResults", -] \ No newline at end of file +] diff --git a/src/codespy/tools/web/client.py b/src/codespy/tools/web/client.py index a5a4d1d..683f548 100644 --- a/src/codespy/tools/web/client.py +++ b/src/codespy/tools/web/client.py @@ -292,4 +292,4 @@ def search_and_fetch(self, query: str, num_results: int = 3) -> list[WebPage]: page = self.fetch_page(result.url) pages.append(page) - return pages \ No newline at end of file + return pages diff --git a/src/codespy/tools/web/models.py b/src/codespy/tools/web/models.py index 4260174..a9a0733 100644 --- a/src/codespy/tools/web/models.py +++ b/src/codespy/tools/web/models.py @@ -55,4 +55,4 @@ def to_markdown(self) -> str: else: lines.append("") - return "\n".join(lines) \ No newline at end of file + return "\n".join(lines) diff --git a/src/codespy/tools/web/server.py b/src/codespy/tools/web/server.py index 05a4980..4bee6e6 100644 --- a/src/codespy/tools/web/server.py +++ b/src/codespy/tools/web/server.py @@ -72,6 +72,6 @@ def search_and_fetch(query: str, num_results: int = 3) -> list[dict]: # Suppress noisy MCP server "Processing request" logs logging.getLogger("mcp.server").setLevel(logging.WARNING) logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - + _browser = WebBrowser() mcp.run() diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py index 522694a..33488d1 100644 --- a/tests/test_config_memory.py +++ b/tests/test_config_memory.py @@ -1,12 +1,10 @@ """Tests for memory storage access verification.""" import sys -from pathlib import Path from unittest.mock import MagicMock, patch import pytest -from codespy.config_dspy import SIGNATURE_NAMES from codespy.config_memory import verify_memory_access from codespy.tools.storage.filesystem.client import FileSystem from codespy.tools.storage.s3.client import S3Client diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index 608c2fa..aa69024 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -1,15 +1,14 @@ """Tests for ContextMemory with Topics.""" from pathlib import Path -from unittest.mock import patch import pytest from codespy.agents.memory.hippocampus import ( ContextMemory, Item, - OpType, Operation, + OpType, Topic, compute_common_ancestor_topic_id, make_topic_id, @@ -460,7 +459,6 @@ def test_topic_dependencies_default_empty(self): def test_topic_deserialization_without_dependencies(self): """Old episodes without dependencies deserialize to empty list.""" - import json old_data = '{"id": "owner/repo/auth", "description": "Auth service"}' topic = Topic.model_validate_json(old_data) assert topic.dependencies == [] diff --git a/tests/test_dspy_config.py b/tests/test_dspy_config.py index fe6ba51..cd7e556 100644 --- a/tests/test_dspy_config.py +++ b/tests/test_dspy_config.py @@ -4,9 +4,7 @@ to avoid heavy import dependencies on dspy, litellm, and other modules. """ -from unittest.mock import patch, MagicMock - -import pytest +from unittest.mock import patch # Standalone copy of the function for isolated testing @@ -27,18 +25,14 @@ def _supports_cache_control(model: str) -> bool: import litellm # noqa: F401 - this is mocked try: info = litellm.get_model_info(model) - if info.get("cache_creation_input_token_cost") is not None: - return True - return False + return info.get("cache_creation_input_token_cost") is not None except Exception: # Model not in LiteLLM DB (Ollama offline, custom endpoint). # Fall back to prefix heuristic. lower = model.lower() if lower.startswith("anthropic/"): return True - if lower.startswith("bedrock/") and "anthropic" in lower: - return True - return False + return bool(lower.startswith("bedrock/") and "anthropic" in lower) class TestSupportsCacheControl: diff --git a/tests/test_patch_utils.py b/tests/test_patch_utils.py index df6c9af..58c9e63 100644 --- a/tests/test_patch_utils.py +++ b/tests/test_patch_utils.py @@ -3,8 +3,6 @@ from pathlib import Path from unittest.mock import MagicMock -import pytest - from codespy.agents.reviewer.models import ScopeResult, ScopeType from codespy.tools.git.models import ChangedFile, FileStatus from codespy.tools.git.patch_utils import ( diff --git a/tests/test_s3_client.py b/tests/test_s3_client.py index ebdfdc7..f23771c 100644 --- a/tests/test_s3_client.py +++ b/tests/test_s3_client.py @@ -1,4 +1,5 @@ import pytest + from codespy.tools.storage.s3.client import S3Client @@ -53,25 +54,25 @@ def _truncate_utf8(raw: bytes, max_bytes: int) -> bytes: def test_truncate_preserves_utf8(self): # "café" = 63 61 66 c3 a9 (5 bytes), max_bytes=4 cuts inside é - raw = "café".encode("utf-8") + raw = "café".encode() result = self._truncate_utf8(raw, 4).decode("utf-8") assert result == "caf" def test_truncate_emoji(self): - raw = "hi🎉bye".encode("utf-8") # 9 bytes + raw = "hi🎉bye".encode() # 9 bytes result = self._truncate_utf8(raw, 4).decode("utf-8") assert result == "hi" def test_truncate_at_exact_boundary_preserves_character(self): """Bug regression: truncation at valid char boundary must NOT strip it.""" # "àè" = c3 a0 c3 a8 (4 bytes), max_bytes=4 lands exactly at end of è - raw = "àè".encode("utf-8") + raw = "àè".encode() result = self._truncate_utf8(raw, 4).decode("utf-8") assert result == "àè" # Both characters preserved (was bug: stripped è) def test_truncate_between_two_multibyte(self): """Truncation between two multi-byte characters preserves the first.""" # "àè" = c3 a0 c3 a8, max_bytes=3 cuts inside è - raw = "àè".encode("utf-8") + raw = "àè".encode() result = self._truncate_utf8(raw, 3).decode("utf-8") assert result == "à" # è is incomplete, stripped diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index ec07ee3..49d8108 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -1,6 +1,5 @@ """Tests for scope_resolver module.""" -import os import tempfile from pathlib import Path @@ -248,7 +247,7 @@ def test_root_manifest_suppresses_indicators_when_sole_manifest(self): scopes, orphans = resolver._resolve(repo_path, changed_files, "owner/repo") # scripts/ indicator is suppressed when root is the only manifest (single-package repo) - scope_subroots = [s.subroot for s in scopes] + [s.subroot for s in scopes] assert len(scopes) == 1 assert scopes[0].subroot == "." From 7abcf289fbd57281da50115a4ed4045b27b154de Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 17 Aug 2026 23:55:13 +0200 Subject: [PATCH 75/79] wip --- poetry.lock | 11 +++++---- pyproject.toml | 8 +++---- src/codespy/tools/storage/s3/client.py | 19 ++++++++++++--- tests/test_s3_client.py | 32 ++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index ac8ee11..0f21fb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1680,15 +1680,18 @@ files = [ [[package]] name = "json-repair" -version = "0.55.2" +version = "0.63.2" description = "A package to repair broken json strings" optional = false python-versions = ">=3.10" files = [ - {file = "json_repair-0.55.2-py3-none-any.whl", hash = "sha256:08109b093fc9fe99b956b8309f9c1dfd2740637056d93df489cc7b4eb56d4286"}, - {file = "json_repair-0.55.2.tar.gz", hash = "sha256:aa5c89692126257efb8e3cf1efe6787387634b5c360fb77313d66f146e980de6"}, + {file = "json_repair-0.63.2-py3-none-any.whl", hash = "sha256:7354c2dd433bf15dedf98d2932ae9cd1ea197d7edc77e6c0538f08e34616580e"}, + {file = "json_repair-0.63.2.tar.gz", hash = "sha256:8385ca04afbf411eebd9f0ba064155d1afe37442aaad52f693825c65cc314586"}, ] +[package.extras] +schema = ["jsonschema (>=4.21)", "pydantic (>=2)"] + [[package]] name = "jsonschema" version = "4.26.0" @@ -4377,4 +4380,4 @@ type = ["pytest-mypy (>=1.0.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "a299c43b1085850017f1a21d51aac5d8e1c77a140989a432d2defd0a2e0a6855" +content-hash = "badd01a46903eaea692f8296a6439cc15e82b881c296135f06605f1b07916459" diff --git a/pyproject.toml b/pyproject.toml index ff9e576..19ad913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.11,<3.14" dspy = {version = "^3.3.0", extras = ["mcp"]} -litellm = "^1.81.6" +litellm = "^1.84.0" cachetools = ">=5.0.0" PyGithub = ">=2.5.0" python-gitlab = ">=4.0.0" @@ -30,11 +30,11 @@ typer = ">=0.12.0" rich = ">=13.9.0" pydantic = ">=2.10.0" pydantic-settings = ">=2.6.0" -gitpython = ">=3.1.0" +gitpython = ">=3.1.41" httpx = ">=0.28.0" boto3 = ">=1.35.0" # Required for AWS Bedrock cloudpickle = "^3.1.2" -json-repair = "^0.55.1" +json-repair = ">=0.56.0" tree-sitter = ">=0.23" tree-sitter-go = ">=0.23" tree-sitter-python = ">=0.23" @@ -48,7 +48,7 @@ tree-sitter-rust = ">=0.23" tree-sitter-hcl = ">=1.2.0" mcp = ">=1.29.0,<2.0.0" beautifulsoup4 = ">=4.12.0" -markdownify = ">=0.13.0" +markdownify = ">=0.14.0" ddgs = ">=8.0.0" [tool.poetry.group.dev.dependencies] diff --git a/src/codespy/tools/storage/s3/client.py b/src/codespy/tools/storage/s3/client.py index 632f579..4b5883f 100644 --- a/src/codespy/tools/storage/s3/client.py +++ b/src/codespy/tools/storage/s3/client.py @@ -4,6 +4,7 @@ import logging import posixpath +from urllib.parse import unquote from codespy.tools.storage.base import Storage from codespy.tools.storage.models import ( @@ -61,10 +62,22 @@ def __init__( # ------------------------------------------------------------------ def _resolve_path(self, path: str) -> str: - stripped = path.strip("/") - # Reject '..' in any path component before normalization - if any(part == ".." for part in stripped.split("/")): + """Resolve and validate a relative path for use as an S3 key. + + Rejects path traversal attempts including percent-encoded variants + (e.g. %2e%2e, %2f..%2f). Returns the normalized original path, + preserving S3 key semantics. + """ + # Decode percent-encoded characters for security validation only. + # This catches encoded traversal attempts (e.g. %2e%2e -> ..) + # without altering the returned S3 key. + decoded = unquote(path) + decoded_stripped = decoded.strip("/") + if any(part == ".." for part in decoded_stripped.split("/")): raise ValueError(f"Path escapes bucket root: {path!r}") + + # Normalize the original (non-decoded) path for the actual S3 key + stripped = path.strip("/") normalised = posixpath.normpath(stripped) if normalised == ".": return "" diff --git a/tests/test_s3_client.py b/tests/test_s3_client.py index f23771c..d05cd4e 100644 --- a/tests/test_s3_client.py +++ b/tests/test_s3_client.py @@ -37,6 +37,38 @@ def test_empty_after_normalization(self): assert self.client._resolve_path(".") == "" assert self.client._resolve_path("/") == "" + def test_rejects_encoded_traversal_lowercase(self): + """Percent-encoded '..' (%2e%2e) must be caught.""" + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("foo/%2e%2e/secret") + + def test_rejects_encoded_traversal_uppercase(self): + """Mixed/uppercase percent-encoding must also be caught.""" + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("foo/%2E%2E/secret") + + def test_rejects_leading_encoded_traversal(self): + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("%2e%2e/etc/passwd") + + def test_rejects_encoded_slash_traversal(self): + """Encoded forward slash creating '..' component when decoded.""" + with pytest.raises(ValueError, match="escapes bucket root"): + self.client._resolve_path("foo%2f..%2fsecret") + + def test_preserves_percent_in_normal_keys(self): + """Legitimate keys with percent characters pass through unchanged.""" + assert self.client._resolve_path("reports/100%25done.txt") == "reports/100%25done.txt" + + def test_encoded_nontraversal_preserved(self): + """Non-traversal encoded chars: original key preserved in return value.""" + assert self.client._resolve_path("foo/%62ar") == "foo/%62ar" + + def test_double_encoded_traversal_passes(self): + """Double-encoded traversal is an opaque S3 key, not actual traversal.""" + # %252e%252e decodes once to %2e%2e (not ..) — legitimate key + assert self.client._resolve_path("foo/%252e%252e/bar") == "foo/%252e%252e/bar" + class TestReadFileTruncation: @staticmethod From a467799cd789551837bc9170181daf0651aa09ce Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 18 Aug 2026 00:10:36 +0200 Subject: [PATCH 76/79] wip --- src/codespy/agents/memory/hippocampus/episode.py | 5 +++++ src/codespy/agents/reviewer/modules/summarizer.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index d0f2ab3..9eb9f8a 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -118,6 +118,7 @@ def find_latest_episode( store: Storage, dir: str, task: str | None = None, + exclude_task: str | None = None, exclude_run_id: str | None = None, ) -> Episode | None: """Find and load the most recent episode for a given scope path. @@ -134,6 +135,8 @@ def find_latest_episode( task: Optional task filter (e.g., "scope", "summary"). Matches ``-{task}-`` substring in filename remainder. If None, any task matches. + exclude_task: Optional task to exclude (e.g., "scope"). + Episodes containing ``-{exclude_task}-`` in filename are skipped. exclude_run_id: If set, skip episodes containing this run_id in filename (avoids loading current pipeline's own episodes). @@ -166,6 +169,8 @@ def find_latest_episode( remainder = entry.name[len(prefix):] if task is not None and f"-{task}-" not in remainder: continue + if exclude_task is not None and f"-{exclude_task}-" in remainder: + continue if exclude_run_id and exclude_run_id in remainder: continue candidates.append(entry) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 0b0932a..08abaef 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -88,7 +88,7 @@ def forward( store = get_memory_store(self._settings) per_scope_memories: list[ContextMemory] = [] for scope in scopes: - ep = find_latest_episode(store, scope.scope_path(), task=None, exclude_run_id=run_id) + ep = find_latest_episode(store, scope.scope_path(), task=None, exclude_task="scope", exclude_run_id=run_id) if ep is not None: per_scope_memories.append(ep.context_memory) if per_scope_memories: From ff47984965b5d28a7be6658812d95624aa0927ba Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 18 Aug 2026 00:25:30 +0200 Subject: [PATCH 77/79] wip --- .env.example | 13 ++++++------- action.yml | 4 ++-- codespy.yaml | 12 ++++++------ docs/configuration.md | 4 ++-- docs/memory.md | 6 +++--- src/codespy/config.py | 3 +-- src/codespy/config_dspy.py | 2 +- 7 files changed, 21 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index c4269f5..bca7ef3 100644 --- a/.env.example +++ b/.env.example @@ -103,16 +103,16 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Cheap (SUMMARY_MODEL): PR summary generation. Simple synthesis. # Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # -# Cheap (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory +# Mid-tier (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory # reflection — summarizing a trajectory and curating the context memory. -# Compact, frequent tasks. -# Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. +# Requires nuance. +# Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. # # By default, all models fall back to DEFAULT_MODEL. To optimize costs: # EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 # SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 -# MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 -# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 +# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 # ============================================================================= @@ -127,8 +127,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # DEFAULT_MAX_ITERS=10 # Provider reasoning budget: minimal | low | medium | high # DEFAULT_REASONING_EFFORT=medium -# Must be 1 while reasoning is enabled (providers reject other values) -# DEFAULT_TEMPERATURE=1 +# DEFAULT_TEMPERATURE=0.2 # Output token budget for a single completion (default: 64000). This is an OUTPUT # ceiling, not a context window, and reasoning/thinking tokens are charged against diff --git a/action.yml b/action.yml index 58a18d9..3b315f0 100644 --- a/action.yml +++ b/action.yml @@ -74,9 +74,9 @@ inputs: default: 'medium' default-temperature: - description: 'Default temperature for LLM calls (must be 1 while reasoning is enabled)' + description: 'Default temperature for LLM calls' required: false - default: '1' + default: '0.2' llm-retries: diff --git a/codespy.yaml b/codespy.yaml index 931984a..3f345ab 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -163,9 +163,9 @@ memory: # Cheap (summary): Used for PR summary generation. Simple synthesis # task. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. # - # Cheap (memory.distiller / memory.cartographer): Memory reflection — - # summarizing a trajectory and curating the context memory. Compact, frequent - # tasks. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. + # Mid-tier (memory.distiller / memory.cartographer): Memory reflection — + # summarizing a trajectory and curating the context memory. Requires nuance. + # Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. # # By default, all models fall back to default_model. Override extraction_model, # the summary model, and the reflection models for cost optimization: @@ -177,9 +177,9 @@ memory: # model: anthropic/claude-haiku-4-5-20251001 # memory: # distiller: -# model: anthropic/claude-haiku-4-5-20251001 +# model: anthropic/claude-sonnet-4-5-20250929 # cartographer: -# model: anthropic/claude-haiku-4-5-20251001 +# model: anthropic/claude-sonnet-4-5-20250929 # ============================================================================ @@ -188,7 +188,7 @@ default_model: anthropic/claude-opus-4-6 # DEFAULT_MODEL extraction_model: null # EXTRACTION_MODEL (falls back to default_model) default_max_iters: 10 # DEFAULT_MAX_ITERS default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) -default_temperature: 1 # DEFAULT_TEMPERATURE (must be 1 while reasoning is enabled) +default_temperature: 0.2 # DEFAULT_TEMPERATURE # Output token budget for a single completion. This is an OUTPUT ceiling, not a # context window, and reasoning/thinking tokens are charged against it — so it diff --git a/docs/configuration.md b/docs/configuration.md index 462ae95..b88f9fc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -109,7 +109,7 @@ AUTO_DISCOVER_GEMINI=false | Model | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Primary model for all signatures | | Reasoning effort | `DEFAULT_REASONING_EFFORT` | `medium` | Provider reasoning budget: `minimal`, `low`, `medium`, `high` | | Max tokens | `DEFAULT_MAX_TOKENS` | `64000` | Output token budget per completion (reasoning tokens included) | -| Temperature | `DEFAULT_TEMPERATURE` | `1` | Must be 1 while reasoning is enabled | +| Temperature | `DEFAULT_TEMPERATURE` | `0.2` | Default temperature for LLM calls | | Max iterations | `DEFAULT_MAX_ITERS` | `10` | Maximum ReAct iterations for tool-using agents | | Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) | @@ -120,7 +120,7 @@ AUTO_DISCOVER_GEMINI=false | Smart | Core analysis & reasoning | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Claude Opus / GPT-5 | | Mid-tier | Field extraction | `EXTRACTION_MODEL` | Falls back to DEFAULT_MODEL | Claude Sonnet | | Cheap | PR summary | `SUMMARY_MODEL` | Falls back to DEFAULT_MODEL | Claude Haiku | -| Cheap | Memory reflection | `MEMORY_DISTILLER_MODEL` / `MEMORY_CARTOGRAPHER_MODEL` | Falls back to DEFAULT_MODEL | Claude Haiku | +| Mid-tier | Memory reflection | `MEMORY_DISTILLER_MODEL` / `MEMORY_CARTOGRAPHER_MODEL` | Falls back to DEFAULT_MODEL | Claude Sonnet | ## Per-Signature Configuration diff --git a/docs/memory.md b/docs/memory.md index bd4b07a..908a80f 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -102,10 +102,10 @@ CODE_REVIEW_MEMORY_ENABLED=true SUMMARY_MEMORY_ENABLED=true ``` -Optimize with cheap reflection model: +Recommended mid-tier reflection model: ```bash -MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 -MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 +MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 +MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ``` --- diff --git a/src/codespy/config.py b/src/codespy/config.py index cc28b1e..5b9815f 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -125,8 +125,7 @@ class Settings(BaseSettings): default_max_iters: int = 10 # Provider reasoning budget; LiteLLM maps this to each provider's native parameter. default_reasoning_effort: ReasoningEffort = "medium" - # Providers require temperature=1 when reasoning is enabled. - default_temperature: float = 1.0 + default_temperature: float = 0.2 # Output token budget per completion. Must be set explicitly: when it is # omitted LiteLLM silently falls back to its own 4096 default, which # truncates reasoning models (thinking tokens are charged against this diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index 0b3fdca..d2ca9a8 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -34,7 +34,7 @@ class SignatureConfig(BaseModel): max_iters: int | None = None model: str | None = None reasoning_effort: ReasoningEffort | None = None # Provider reasoning budget - temperature: float | None = None # Must be 1 when reasoning is enabled + temperature: float | None = None max_tokens: int | None = None # Output token budget (reasoning tokens included) scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests From 8c7eaeabb7f92fd12c4356e4a0ec7f74546a9d63 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 18 Aug 2026 00:27:43 +0200 Subject: [PATCH 78/79] wip --- .github/workflows/codespy-review.yml.example | 20 ++- action.yml | 123 ++++++++++++++++++- docs/memory.md | 39 ++++++ 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codespy-review.yml.example b/.github/workflows/codespy-review.yml.example index 78f8da6..dcb70ac 100644 --- a/.github/workflows/codespy-review.yml.example +++ b/.github/workflows/codespy-review.yml.example @@ -150,4 +150,22 @@ jobs: # if: steps.codespy.outputs.issues-count > 10 # run: | # echo "Too many issues found!" -# exit 1 \ No newline at end of file +# exit 1 + +# --- Using Memory (S3 persistence across runs) --- +# - name: Run CodeSpy Review +# uses: khezen/codespy@main +# with: +# model: 'anthropic/claude-opus-4-6' +# anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} +# # AWS credentials for S3 memory backend +# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} +# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} +# aws-region: 'us-east-1' +# # Enable memory globally +# memory-enabled: 'true' +# memory-backend: 's3' +# memory-s3-bucket: 'my-codespy-memory' +# # Use cheap model for reflection +# memory-distiller-model: 'anthropic/claude-haiku-4-5-20251001' +# memory-cartographer-model: 'anthropic/claude-haiku-4-5-20251001' \ No newline at end of file diff --git a/action.yml b/action.yml index 3b315f0..e7dbc9f 100644 --- a/action.yml +++ b/action.yml @@ -210,7 +210,86 @@ inputs: summary-temperature: description: 'Temperature for summary' required: false - + + # ========================================== + # MEMORY (Hippocampus) + # ========================================== + memory-enabled: + description: 'Enable memory (Hippocampus) globally for cross-run learning (default: false)' + required: false + default: 'false' + + memory-backend: + description: 'Memory storage backend: filesystem or s3. Note: filesystem is ephemeral in Docker; use s3 for persistence across runs.' + required: false + default: 'filesystem' + + memory-root: + description: 'Filesystem path for memory storage (only used with filesystem backend; ephemeral in Docker)' + required: false + + memory-s3-bucket: + description: 'S3 bucket for memory storage (required when backend is s3). Requires aws-access-key-id/aws-secret-access-key inputs.' + required: false + + memory-s3-region: + description: 'S3 region for memory bucket (defaults to aws-region input)' + required: false + + memory-s3-endpoint-url: + description: 'S3 endpoint URL for S3-compatible stores (e.g., MinIO)' + required: false + + memory-max-reflects: + description: 'Max online reflections per agent call (0 = batch-only, default: 0)' + required: false + default: '0' + + memory-distiller-model: + description: 'Model for the Distiller reflection module (smaller model recommended)' + required: false + + memory-distiller-reasoning-effort: + description: 'Reasoning effort for Distiller (minimal|low|medium|high)' + required: false + + memory-distiller-temperature: + description: 'Temperature for Distiller' + required: false + + memory-cartographer-model: + description: 'Model for the Cartographer reflection module (smaller model recommended)' + required: false + + memory-cartographer-reasoning-effort: + description: 'Reasoning effort for Cartographer (minimal|low|medium|high)' + required: false + + memory-cartographer-temperature: + description: 'Temperature for Cartographer' + required: false + + # Per-signature memory overrides + scope-memory-enabled: + description: 'Enable memory for scope identification (overrides global memory-enabled)' + required: false + + code-review-memory-enabled: + description: 'Enable memory for code review (overrides global memory-enabled)' + required: false + + doc-memory-enabled: + description: 'Enable memory for doc review (overrides global memory-enabled)' + required: false + + supply-chain-memory-enabled: + description: 'Enable memory for supply chain (overrides global memory-enabled)' + required: false + + summary-memory-enabled: + description: 'Enable memory for summary (overrides global memory-enabled)' + required: false + excluded-directories: description: 'JSON array of directories to exclude from review (e.g., ["vendor", "dist"])' required: false @@ -319,6 +398,26 @@ runs: # Other settings EXCLUDED_DIRECTORIES: ${{ inputs.excluded-directories }} + + # Memory (Hippocampus) + MEMORY_DEFAULT_ENABLED: ${{ inputs.memory-enabled }} + MEMORY_BACKEND: ${{ inputs.memory-backend }} + MEMORY_ROOT: ${{ inputs.memory-root }} + MEMORY_S3_BUCKET: ${{ inputs.memory-s3-bucket }} + MEMORY_S3_REGION: ${{ inputs.memory-s3-region }} + MEMORY_S3_ENDPOINT_URL: ${{ inputs.memory-s3-endpoint-url }} + MEMORY_DEFAULT_MAX_REFLECTS: ${{ inputs.memory-max-reflects }} + MEMORY_DISTILLER_MODEL: ${{ inputs.memory-distiller-model }} + MEMORY_DISTILLER_REASONING_EFFORT: ${{ inputs.memory-distiller-reasoning-effort }} + MEMORY_DISTILLER_TEMPERATURE: ${{ inputs.memory-distiller-temperature }} + MEMORY_CARTOGRAPHER_MODEL: ${{ inputs.memory-cartographer-model }} + MEMORY_CARTOGRAPHER_REASONING_EFFORT: ${{ inputs.memory-cartographer-reasoning-effort }} + MEMORY_CARTOGRAPHER_TEMPERATURE: ${{ inputs.memory-cartographer-temperature }} + SCOPE_MEMORY_ENABLED: ${{ inputs.scope-memory-enabled }} + CODE_REVIEW_MEMORY_ENABLED: ${{ inputs.code-review-memory-enabled }} + DOC_MEMORY_ENABLED: ${{ inputs.doc-memory-enabled }} + SUPPLY_CHAIN_MEMORY_ENABLED: ${{ inputs.supply-chain-memory-enabled }} + SUMMARY_MEMORY_ENABLED: ${{ inputs.summary-memory-enabled }} run: | set -eo pipefail @@ -379,7 +478,27 @@ runs: # Other settings [ -n "$EXCLUDED_DIRECTORIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e EXCLUDED_DIRECTORIES" - + + # Memory (Hippocampus) + [ -n "$MEMORY_DEFAULT_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DEFAULT_ENABLED" + [ -n "$MEMORY_BACKEND" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_BACKEND" + [ -n "$MEMORY_ROOT" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_ROOT" + [ -n "$MEMORY_S3_BUCKET" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_S3_BUCKET" + [ -n "$MEMORY_S3_REGION" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_S3_REGION" + [ -n "$MEMORY_S3_ENDPOINT_URL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_S3_ENDPOINT_URL" + [ -n "$MEMORY_DEFAULT_MAX_REFLECTS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DEFAULT_MAX_REFLECTS" + [ -n "$MEMORY_DISTILLER_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_MODEL" + [ -n "$MEMORY_DISTILLER_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_REASONING_EFFORT" + [ -n "$MEMORY_DISTILLER_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_TEMPERATURE" + [ -n "$MEMORY_CARTOGRAPHER_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_MODEL" + [ -n "$MEMORY_CARTOGRAPHER_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_REASONING_EFFORT" + [ -n "$MEMORY_CARTOGRAPHER_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_TEMPERATURE" + [ -n "$SCOPE_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MEMORY_ENABLED" + [ -n "$CODE_REVIEW_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MEMORY_ENABLED" + [ -n "$DOC_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MEMORY_ENABLED" + [ -n "$SUPPLY_CHAIN_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MEMORY_ENABLED" + [ -n "$SUMMARY_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MEMORY_ENABLED" + # Build codespy command arguments CMD_ARGS="review ${{ steps.pr-url.outputs.url }}" CMD_ARGS="$CMD_ARGS --output ${{ inputs.output-format }}" diff --git a/docs/memory.md b/docs/memory.md index 908a80f..71bdfbb 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -108,6 +108,45 @@ MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ``` +### GitHub Action + +Enable memory with S3 persistence: +```yaml +- name: Run CodeSpy Review + uses: khezen/codespy@v1 + with: + model: 'anthropic/claude-opus-4-6' + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + # AWS credentials (required for S3 memory backend) + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: 'us-east-1' + # Memory + memory-enabled: 'true' + memory-backend: 's3' + memory-s3-bucket: 'my-codespy-memory' + memory-distiller-model: 'anthropic/claude-haiku-4-5-20251001' + memory-cartographer-model: 'anthropic/claude-haiku-4-5-20251001' +``` + +Enable only for code review (per-signature override): +```yaml +- name: Run CodeSpy Review + uses: khezen/codespy@v1 + with: + model: 'anthropic/claude-opus-4-6' + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + memory-backend: 's3' + memory-s3-bucket: 'my-codespy-memory' + code-review-memory-enabled: 'true' + memory-distiller-model: 'anthropic/claude-haiku-4-5-20251001' + memory-cartographer-model: 'anthropic/claude-haiku-4-5-20251001' +``` + +> **Note:** The `filesystem` backend is ephemeral in the GitHub Action (Docker container is removed after each run). Use `s3` for persistent memory across reviews. + --- [← Back to README](../README.md#documentation) From 1a5c5ef4456c64af89b314ab9e2d464c93c4848e Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 18 Aug 2026 00:34:36 +0200 Subject: [PATCH 79/79] release --- .github/workflows/ci.yml | 7 +++++ CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 ++-- src/codespy/__init__.py | 2 +- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db924cc..234bae7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,13 @@ jobs: git tag -f "${{ needs.detect-version-bump.outputs.version }}" git push --force origin "${{ needs.detect-version-bump.outputs.version }}" + - name: Update major version tag + run: | + VERSION="${{ needs.detect-version-bump.outputs.version }}" + MAJOR="${VERSION%%.*}" + git tag -f "v${MAJOR}" + git push --force origin "v${MAJOR}" + - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c169be7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +## [1.0.0] - 2026-08-18 + +### Added +- Cross-review memory system (Hippocampus) with S3/filesystem storage backends +- Context window overflow resilience (`ContextSafe` wrapper with automatic RLM fallback) +- Scope resolver: deterministic analysis + ReAct agent refinement (replaces `ScopeIdentifier`) +- Patch compaction: expands diff hunks to enclosing function boundaries via Tree-sitter +- Deterministic package manifest parser: extracts package identity from 25+ formats without LLM (npm, Go, pip, Cargo, Maven, Gradle, Composer, Bundler, NuGet, Swift, Pub, Hex, Helm, etc.) +- Tree-sitter extractors for Bash, C++, C#, PHP, Ruby +- Ripgrep fallback extractor for languages without Tree-sitter grammar +- Unified storage abstraction layer (`tools/storage/`) with filesystem and S3 backends +- Audit signature: dedicated module for review quality assessment and recommendation +- Reasoning effort configuration (`minimal|low|medium|high`) — maps to provider-native parameters (Anthropic thinking budget, OpenAI reasoning_effort) +- Per-signature `max_tokens` output token budget (replaces `max_reasoning_tokens`) +- TwoStepAdapter with dedicated extraction model for structured field extraction +- Memory storage access verification (S3/filesystem connectivity check at startup) +- Sparse checkout support in scope resolver for large monorepos +- Deno runtime in Docker image (required by DSPy RLM sandbox) +- Full documentation suite: architecture, configuration, development, memory, usage +- GitHub Action: reasoning effort, summary, and temperature inputs + +### Security +- S3 path traversal hardening: `_resolve_path` decodes percent-encoded input before validation (catches `%2e%2e`, `%2f..%2f` — CWE-22) +- `json-repair` pinned to >=0.56.0 (GHSA-xf7x-x43h-rpqh) +- `litellm` floor raised to ^1.84.0 (excludes known-vulnerable versions) +- `gitpython` floor raised to >=3.1.41 (excludes CVE-affected versions) +- `markdownify` floor raised to >=0.14.0 (excludes known-vulnerable versions) + +### Changed +- **BREAKING**: `MergeRequest` model renamed to `PullRequest` (backward-compat alias removed) +- **BREAKING**: `ReviewContext.merge_request` field → `pull_request` (compat property removed) +- **BREAKING**: CLI argument `mr_url` → `pr_url`; `fetch_merge_request()` → `fetch_pull_request()` +- **BREAKING**: `build_mr_from_diff()` → `build_pr_from_diff()` +- **BREAKING**: `ReviewResult` fields: `mr_number`→`pr_number`, `mr_title`→`pr_title`, `mr_url`→`pr_url` +- **BREAKING**: MCP tool `review_pr` parameter renamed: `mr_url` → `pr_url` +- **BREAKING**: `tools/filesystem` module moved to `tools/storage/filesystem` (import path changed) +- **BREAKING**: Per-signature `max_context_size` and `max_reasoning_tokens` env vars replaced by `reasoning_effort`, `temperature`, and `max_tokens` +- **BREAKING**: GitHub Action inputs removed: `*-max-context-size`, `*-max-reasoning-tokens` (replaced by `*-reasoning-effort`) +- Docker base image: Alpine → Debian slim (glibc required by Deno) +- `dspy` dependency: ^3.1.3 → ^3.3.0 +- `mcp` dependency: >=1.0.0 → >=1.29.0,<2.0.0 +- `litellm` dependency: ^1.81.6 → ^1.84.0 +- `gitpython` dependency: >=3.1.0 → >=3.1.41 +- `json-repair` dependency: ^0.55.1 → >=0.56.0 +- `markdownify` dependency: >=0.13.0 → >=0.14.0 +- ScopeIdentifierSignature → ScopeRefinementSignature (extracted to `ScopeResolver` module) +- MRSummarySignature → PRSummarySignature (extracted to `Summarizer` module with config key `summary`) +- `ReviewMetadata` model introduced to reduce parameter proliferation +- Per-module overflow detection replaced by centralized `ContextSafe` wrapper +- README rewritten: detailed sections moved to `docs/`, simplified TOC +- `codespy.yaml` expanded with memory, reasoning, and per-signature configuration (194 → 326 lines) + +### Removed +- `ScopeIdentifier` module (replaced by `ScopeResolver`) +- `tools/filesystem/__init__.py` (replaced by `tools/storage/` abstraction) +- `default_max_context_size` and `default_max_reasoning_tokens` settings +- Per-signature `MAX_CONTEXT_SIZE` and `MAX_REASONING_TOKENS` env vars (replaced by `REASONING_EFFORT` and `MAX_TOKENS`) +- `MergeRequest` backward-compat alias +- Per-module `_would_overflow_context()` methods diff --git a/pyproject.toml b/pyproject.toml index 19ad913..c34506b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "0.4.1" +version = "1.0.0" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" @@ -11,12 +11,13 @@ documentation = "https://github.com/khezen/codespy#readme" keywords = ["code-review", "ai", "dspy", "llm", "github", "pull-request", "security", "bug-detection", "static-analysis"] packages = [{ include = "codespy", from = "src" }] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] [tool.poetry.dependencies] diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index 2512192..6da04e5 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "0.4.1" +__version__ = "1.0.0"