From 3244bc7abc302f02531869114f6c2319a24200a6 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Mon, 31 Aug 2026 16:02:59 +0200 Subject: [PATCH 01/14] experience->action --- src/codespy/agents/memory/hippocampus/budget.py | 2 +- .../agents/memory/hippocampus/context_memory.py | 10 +++++----- .../memory/hippocampus/modules/cartographer.py | 2 +- .../memory/hippocampus/modules/distiller.py | 16 +++------------- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 87ee0b0..cd5bfc7 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -75,7 +75,7 @@ class MemoryBudget: # Eviction priority — lower number = evict first _SECTION_EVICT_PRIORITY: dict[str, int] = { "parsing_schema": 0, # evict first — cheap to rediscover - "experiences": 1, # agent-derived tool-use patterns; can be re-observed + "actions": 1, # agent-derived tool-use action patterns; can be re-observed "reusable_results": 2, # agent-derived; can be recomputed "domain_constants": 3, # exact values worth protecting "context_roadmap": 4, # protected — structural index diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index c072d30..c813c5f 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -56,7 +56,7 @@ def _missing_(cls, value: object) -> OpType | None: "context_roadmap", "context_understanding", "domain_constants", - "experiences", + "actions", "parsing_schema", "reusable_results", ] @@ -66,7 +66,7 @@ def _missing_(cls, value: object) -> OpType | None: "context_roadmap": "cr", "context_understanding": "cu", "domain_constants": "dc", - "experiences": "ex", + "actions": "ac", "parsing_schema": "ps", "reusable_results": "rr", } @@ -107,7 +107,7 @@ class CacheCandidate(BaseModel): default="context_understanding", description=( "Target section: context_understanding, domain_constants, " - "context_roadmap, reusable_results, parsing_schema, or experiences" + "context_roadmap, reusable_results, parsing_schema, or actions" ), ) value: str = Field( @@ -193,10 +193,10 @@ class ContextMemory(BaseModel): "that multiple questions would need" ), ) - experiences: list[Item] = Field( + actions: list[Item] = Field( default_factory=list, description=( - "Tool execution experiences: what tool was used, for what purpose, " + "Tool execution action patterns: what tool was used, for what purpose, " "and what the result was. Helps avoid redundant tool calls in future runs." ), ) diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index 4b6b3a6..3250e21 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -78,7 +78,7 @@ class CartographerSig(dspy.Signature): distributions, classifications) from processing the full context that multiple questions would need. Note the computation method to judge reliability. - 5. experiences — tool execution patterns (what tool, what purpose, + 5. actions — tool execution action patterns (what tool, what purpose, what result) that transfer across runs. Evict when the tool-use pattern is obvious or no longer relevant. 6. parsing_schema — format observations, delimiters, splitting diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 2893119..42f8215 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -75,18 +75,8 @@ class DistillerSig(dspy.Signature): is about — genre, time period, key themes, nature of the data — that frames any question - Medium value: - - Experiences: tool execution patterns that transfer across runs. - Record what tool was used, for what purpose, and what the result - was. Focus on tool-use strategies that would save a future agent - exploration work. Do NOT record every individual tool call — only patterns - that a future run on the same context would benefit from. - - 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. + Medium value: + - Actions: tool execution action patterns that transfer across runs. Do NOT cache: - Facts that answer only one specific question (e.g., a verbatim @@ -103,7 +93,7 @@ class DistillerSig(dspy.Signature): Assign each candidate to one of these exact section names (they map onto the context memory schema): context_understanding, domain_constants, - context_roadmap, reusable_results, parsing_schema, experiences. + context_roadmap, reusable_results, parsing_schema, actions. Each candidate is an object with exactly these fields: - section: one of the six section names above From 2ff5e44964386ab130f067e48900bdd73a3794eb Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 8 Sep 2026 15:09:34 +0200 Subject: [PATCH 02/14] move episodic memory to postgres --- .env.example | 10 +- CHANGELOG.md | 21 + action.yml | 19 +- codespy.yaml | 19 +- docs/configuration.md | 22 +- docs/memory.md | 17 +- pyproject.toml | 2 +- src/codespy/__init__.py | 2 +- .../agents/memory/hippocampus/budget.py | 10 +- .../memory/hippocampus/context_memory.py | 82 --- .../agents/memory/hippocampus/hippocampus.py | 45 +- .../hippocampus/modules/cartographer.py | 5 +- .../memory/hippocampus/modules/distiller.py | 19 +- src/codespy/agents/reviewer/models.py | 18 +- .../agents/reviewer/modules/auditor.py | 10 +- .../agents/reviewer/modules/code_reviewer.py | 5 +- .../agents/reviewer/modules/doc_reviewer.py | 5 +- .../agents/reviewer/modules/helpers.py | 1 - .../reviewer/modules/manifest_parser.py | 578 ------------------ .../agents/reviewer/modules/scope_resolver.py | 49 +- .../agents/reviewer/modules/summarizer.py | 33 +- .../reviewer/modules/supply_chain_auditor.py | 5 +- src/codespy/agents/reviewer/reviewer.py | 25 +- src/codespy/config.py | 4 + src/codespy/config_memory.py | 4 +- tests/test_config.py | 57 ++ tests/test_context_memory.py | 158 ----- 27 files changed, 255 insertions(+), 970 deletions(-) create mode 100644 tests/test_config.py diff --git a/.env.example b/.env.example index d0df993..d78daed 100644 --- a/.env.example +++ b/.env.example @@ -155,6 +155,10 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Minimum confidence threshold for reported issues (0.0-1.0) # MIN_CONFIDENCE=0.81 +# Expand diff context to full function bodies (Tree-sitter) before review. +# When false (default), reviewers see original PR patches. +# COMPACT_PATCHES=false + # ============================================================================= # RLM Fallback (Context Rot Prevention) # ============================================================================= @@ -216,13 +220,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_MAX_CONTEXT_ITEM_TOKENS it gives the memory's item -# capacity (16384 / 410 ~= 39 items). +# capacity (16384 / 512 = 32 items). # MEMORY_MAX_CONTEXT_MEMORY_TOKENS=16384 # 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_MAX_CONTEXT_ITEM_TOKENS=410 +# MEMORY_MAX_CONTEXT_ITEM_TOKENS=512 # 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-15% of the reflection model's context window. Unset = full trajectory @@ -311,6 +315,8 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # SUPPLY_CHAIN_MEMORY_MAX_REFLECTS=1 # DOC_ENABLED=true +# DOC_MAX_ITERS=1 +# DOC_MAX_LLM_CALLS=2 # DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 # DOC_MEMORY_ENABLED=true # DOC_MEMORY_MAX_REFLECTS=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c31085..8be7baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ ## [Unreleased] +## [1.0.15] - 2026-09-01 + +### Changed +- Renamed `experiences` section to `actions` in `ContextMemory` (section prefix `"ex"` → `"ac"`) +- `max_context_item_tokens` default: `410` → `512` (capacity: ~32 items with 16384 context memory tokens) +- `Hippocampus` init parameter `topic_ids: list[str]` → `topics: list[Topic]` — topics auto-registered in context memory +- `Summarizer.forward()` signature simplified: individual PR fields replaced with `pr_context: PRContext` parameter +- Scope resolver no longer builds dependency graphs between scopes +- Per-signature iteration defaults: `doc` (max_iters=1, max_llm_calls=2), `scope` (max_iters=3, max_llm_calls=5), `code_review` (max_iters=5, max_llm_calls=8) + +### Added +- `PRContext.pr_url` and `PRContext.pr_description` fields +- `PRContext.to_topic()` helper — builds a `Topic` from PR metadata for cross-review memory +- PR URL topic automatically registered across all pipeline modules (scope resolver, summarizer, code reviewer, doc reviewer, supply chain auditor, auditor) +- GitHub Action inputs: `doc-max-iters`, `doc-max-llm-calls` + +### Removed +- `Topic.dependencies` field and all dependency resolution logic in scope resolver +- `ScopeResult.is_dependency` field +- Dependency extraction from `manifest_parser.py` (~578 lines): `PACKAGE_MANAGER_TO_ECOSYSTEM`, `extract_dependencies()`, `infer_repo_from_name()`, and 13 per-language `_extract_deps_from_*` functions — only `extract_package_name()` retained + ## [1.0.14] - 2026-08-31 ### Fixed diff --git a/action.yml b/action.yml index 3224268..bf5bb60 100644 --- a/action.yml +++ b/action.yml @@ -128,6 +128,11 @@ inputs: required: false default: '0.81' + compact-patches: + description: 'Expand diff context to full function bodies via Tree-sitter before review (default: false)' + required: false + default: 'false' + # ========================================== # SIGNATURE: scope # ========================================== @@ -195,7 +200,15 @@ inputs: doc-model: description: 'Model for doc review (empty = use default)' required: false - + + doc-max-iters: + description: 'Max iterations for doc review' + required: false + + doc-max-llm-calls: + description: 'Max LLM calls for doc review RLM fallback' + required: false + doc-reasoning-effort: description: 'Reasoning effort for doc (minimal|low|medium|high)' required: false @@ -466,6 +479,7 @@ runs: ENABLE_PROMPT_CACHING: ${{ inputs.enable-prompt-caching }} DEFAULT_MAX_LLM_CALLS: ${{ inputs.default-max-llm-calls }} MIN_CONFIDENCE: ${{ inputs.min-confidence }} + COMPACT_PATCHES: ${{ inputs.compact-patches }} # RLM fallback RLM_FALLBACK_ENABLED: ${{ inputs.rlm-fallback-enabled }} @@ -492,6 +506,8 @@ runs: # Doc review signature DOC_ENABLED: ${{ inputs.doc-enabled }} DOC_MODEL: ${{ inputs.doc-model }} + DOC_MAX_ITERS: ${{ inputs.doc-max-iters }} + DOC_MAX_LLM_CALLS: ${{ inputs.doc-max-llm-calls }} DOC_REASONING_EFFORT: ${{ inputs.doc-reasoning-effort }} DOC_TEMPERATURE: ${{ inputs.doc-temperature }} @@ -573,6 +589,7 @@ runs: [ -n "$ENABLE_PROMPT_CACHING" ] && DOCKER_ARGS="$DOCKER_ARGS -e ENABLE_PROMPT_CACHING" [ -n "$DEFAULT_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_LLM_CALLS" [ -n "$MIN_CONFIDENCE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MIN_CONFIDENCE" + [ -n "$COMPACT_PATCHES" ] && DOCKER_ARGS="$DOCKER_ARGS -e COMPACT_PATCHES" # RLM fallback [ -n "$RLM_FALLBACK_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_ENABLED" diff --git a/codespy.yaml b/codespy.yaml index ba88ac5..3506792 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -48,6 +48,10 @@ llm: # Issues below this threshold are silently discarded. min_confidence: 0.81 # MIN_CONFIDENCE +# Expand diff context to full function bodies (Tree-sitter) before review. +# When false (default), reviewers see the original PR patches. +compact_patches: false # COMPACT_PATCHES + # ============================================================================ # GIT PLATFORMS @@ -110,7 +114,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 (16384 / 410 ~= 39 items). + # gives the memory's item capacity (16384 / 512 = 32 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 @@ -124,7 +128,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. max_context_memory_tokens: 16384 # MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_item_tokens: 410 # MEMORY_MAX_CONTEXT_ITEM_TOKENS + max_context_item_tokens: 512 # MEMORY_MAX_CONTEXT_ITEM_TOKENS max_trajectory_tokens: 16384 # MEMORY_MAX_TRAJECTORY_TOKENS max_question_tokens: 8192 # MEMORY_MAX_QUESTION_TOKENS compact_trajectory: true # MEMORY_COMPACT_TRAJECTORY @@ -259,8 +263,8 @@ signatures: # Unified code review: bugs, security vulnerabilities, and code smells in a single agent pass per scope code_review: enabled: true # CODE_REVIEW_ENABLED - max_iters: null # CODE_REVIEW_MAX_ITERS - max_llm_calls: null # CODE_REVIEW_MAX_LLM_CALLS + max_iters: 5 # CODE_REVIEW_MAX_ITERS + max_llm_calls: 8 # CODE_REVIEW_MAX_LLM_CALLS model: null # CODE_REVIEW_MODEL reasoning_effort: null # CODE_REVIEW_REASONING_EFFORT temperature: null # CODE_REVIEW_TEMPERATURE @@ -273,7 +277,8 @@ signatures: # Note: doc extraction is now deterministic (no LLM) — see doc_extractor.py doc: enabled: true # DOC_ENABLED - max_llm_calls: null # DOC_MAX_LLM_CALLS + max_iters: 1 # DOC_MAX_ITERS + max_llm_calls: 2 # DOC_MAX_LLM_CALLS model: null # DOC_MODEL reasoning_effort: null # DOC_REASONING_EFFORT temperature: null # DOC_TEMPERATURE @@ -285,8 +290,8 @@ signatures: # Scope Identifier signature scope: enabled: true # SCOPE_ENABLED - max_iters: null # SCOPE_MAX_ITERS - max_llm_calls: null # SCOPE_MAX_LLM_CALLS + max_iters: 3 # SCOPE_MAX_ITERS + max_llm_calls: 5 # SCOPE_MAX_LLM_CALLS model: null # SCOPE_MODEL reasoning_effort: null # SCOPE_REASONING_EFFORT temperature: null # SCOPE_TEMPERATURE diff --git a/docs/configuration.md b/docs/configuration.md index 85536cd..14a0f75 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -110,7 +110,7 @@ AUTO_DISCOVER_GEMINI=false | 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` | `0.2` | Default temperature for LLM calls | -| Max iterations | `DEFAULT_MAX_ITERS` | `3` | Maximum ReAct iterations for tool-using agents | +| Max iterations | `DEFAULT_MAX_ITERS` | `5` | Maximum ReAct iterations for tool-using agents | | Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) | | RLM fallback | `RLM_FALLBACK_ENABLED` | `true` | Proactive RLM fallback for context rot prevention | | RLM react threshold | `RLM_FALLBACK_REACT_THRESHOLD` | `0.30` | Context ratio triggering RLM for ReAct modules | @@ -132,12 +132,12 @@ 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 | +| Scope Identifier | `scope` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| PR Summary | `summary` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Code Reviewer | `code_review` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Doc Reviewer | `doc` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | +| Supply Chain | `supply_chain` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS, SCAN_UNCHANGED | +| Auditor | `audit` | ENABLED, MAX_ITERS, MAX_LLM_CALLS, MODEL, REASONING_EFFORT, TEMPERATURE, MAX_TOKENS | Example: `CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929` @@ -175,10 +175,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 | +| Context memory tokens | `MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS` | `16384` | Max tokens for persisted context memory | +| Item tokens | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | `512` | Soft per-item token limit | +| Trajectory tokens | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | `16384` | Cap on trajectory fed to Distiller | +| Question tokens | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | `8192` | Cap on serialized reflection inputs | See [Memory System](memory.md) for full memory configuration details. diff --git a/docs/memory.md b/docs/memory.md index da90bea..ceccfe3 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -24,13 +24,14 @@ parsing schemas — and reuse it in subsequent reviews of the same code area. ### Context Memory -Five sections (from general to specific): +Six 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 +4. **`actions`** — Tool execution patterns and action history +5. **`parsing_schema`** — File format conventions, naming patterns, structural rules +6. **`reusable_results`** — Computed facts reusable in future reviews Each section contains Items with tags (general, scope-specific) and text content. @@ -51,12 +52,12 @@ Reflection iterates `max_reflects` times (0 = reflect once at end_episode). | 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 | +| Context memory | `MEMORY_DEFAULT_MAX_CONTEXT_MEMORY_TOKENS` | 16384 | Ceiling on persisted ContextMemory (re-sent every iteration) | +| Item | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | 512 | Soft per-item limit (expressed to LLM, not truncated) | +| Trajectory | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | 16384 | Head+tail cap on trajectory fed to Distiller | +| Question | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | 8192 | Cap on serialized inputs as reflection question | -Item capacity ≈ context_memory_tokens / item_tokens (8192/410 ≈ 19 items) +Item capacity ≈ context_memory_tokens / item_tokens (16384/512 = 32 items) ## Configuration diff --git a/pyproject.toml b/pyproject.toml index 086f2f4..50c3628 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.0.14" +version = "1.0.15" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index e32980e..47ad5af 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.0.14" +__version__ = "1.0.15" diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index cd5bfc7..3b83c1c 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -26,13 +26,13 @@ class MemoryBudget: once (see ``Settings.get_memory_budget``) and shared across instances. Attributes: - max_context_memory_tokens: Hard ceiling on the rendered ContextMemory, + max_context_memory_tokens: Hard ceiling on the serialized 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 memory's approximate item capacity (16384 / 410 ~= 39 items). + gives the memory's approximate item capacity (16384 / 512 = 32 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 @@ -66,7 +66,7 @@ class MemoryBudget: """ max_context_memory_tokens: int = 16384 - max_context_item_tokens: int = 410 + max_context_item_tokens: int = 512 max_trajectory_tokens: int | None = 16384 max_question_tokens: int | None = 8192 compact_trajectory: bool = True @@ -122,7 +122,7 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: def evict(context_memory: ContextMemory, scores: dict[str, int], budget: int) -> ContextMemory: - if count_tokens(context_memory.render()) <= budget: + if count_tokens(context_memory.model_dump_json()) <= budget: return context_memory item_section: dict[str, str] = { it.id: sec for sec in context_memory.section_names() for it in context_memory.section(sec) @@ -141,7 +141,7 @@ def evict(context_memory: ContextMemory, scores: dict[str, int], budget: int) -> for v in victims: removed.add(v.id) trial = context_memory.without(removed) - if count_tokens(trial.render()) <= budget: + if count_tokens(trial.model_dump_json()) <= budget: return trial return context_memory.without(removed) diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index c813c5f..5183bd3 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -80,9 +80,6 @@ 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): @@ -245,85 +242,6 @@ 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]]: diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 5682c00..98ef7cd 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -22,6 +22,7 @@ Mutation, Operation, OpType, + Topic, ) from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.hippocampus.episode import load_episode as _load_episode @@ -34,20 +35,13 @@ 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. - """ + """Prepend context_memory field to signature.""" return sig.prepend( name="context_memory", field=dspy.InputField( - desc=( - "Current context memory (topic-grouped, with item IDs and sections). " - "Use it before redundant tool calls." - ) + desc="Current context memory. Use it before redundant tool calls." ), - type_=str, + type_=ContextMemory, ) @@ -134,7 +128,7 @@ def __init__( task_name: str | None = None, run_id: str | None = None, initial_memory: ContextMemory | None = None, - topic_ids: list[str] | None = None, + topics: list[Topic] | None = None, ): """ Args: @@ -171,8 +165,9 @@ class for per-field guidance. Resolve one from configuration with 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. + topics: Optional list of Topic objects to register in the context memory. + Topic IDs are auto-assigned to all new items created during this episode. + Used for scope-aware memory organization. """ super().__init__() @@ -201,7 +196,14 @@ class for per-field guidance. Resolve one from configuration with self.max_reflects = max_reflects self.question = question self.cmem = initial_memory.model_copy(deep=True) if initial_memory else ContextMemory() - self._topic_ids = topic_ids or [] + self._topic_ids: list[str] = [] + if topics: + existing_ids = {t.id for t in self.cmem.topics} + for topic in topics: + self._topic_ids.append(topic.id) + if topic.id not in existing_ids: + self.cmem.topics.append(topic) + existing_ids.add(topic.id) self.scores: dict[str, int] = {} # Buffer of per-call bounded trajectory strings, cleared after end_episode(). self._episode_trajectories: list[str] = [] @@ -244,13 +246,8 @@ class for per-field guidance. Resolve one from configuration with # Step counter incremented per _distill() call for mutation grouping. self._distill_step: int = 0 - @property - 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_memory=self.cmem.render(), **kwargs) + pred = self.agent(context_memory=self.cmem, **kwargs) self._buffer_and_distill(pred, kwargs) return pred @@ -263,7 +260,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_memory=self.cmem.render(), **kwargs) + pred = await self.agent.acall(context_memory=self.cmem, **kwargs) await asyncio.to_thread(self._buffer_and_distill, pred, kwargs) return pred @@ -578,7 +575,7 @@ def _update_item_scores(self, tags: dict[str, ItemTag]) -> None: def _distill(self, trajectory: str, question: str) -> None: distilled = self.distill( trajectory=trajectory, - context_memory=self.cmem.render(), + context_memory=self.cmem, question=question, max_context_item_tokens=self.budget.max_context_item_tokens, ) @@ -591,12 +588,12 @@ def _distill(self, trajectory: str, question: str) -> None: diagnosis=distilled.diagnosis, item_tags=tags, cache_candidates=list(distilled.cache_candidates or []), - current_map=self.cmem.render(), + current_map=self.cmem, 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_memory_tokens, - current_tokens=count_tokens(self.cmem.render()), + current_tokens=count_tokens(self.cmem.model_dump_json()), max_context_item_tokens=self.budget.max_context_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 3250e21..4f7b7bf 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -5,6 +5,7 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, + ContextMemory, ItemTag, Operation, ) @@ -111,8 +112,8 @@ class CartographerSig(dspy.Signature): cache_candidates: list[CacheCandidate] = dspy.InputField( desc="Candidate items the Distiller proposed." ) - current_map: str = dspy.InputField( - desc="Current context memory (topic-grouped, with item IDs and sections)." + current_map: ContextMemory = dspy.InputField( + desc="Current context memory." ) question: str = dspy.InputField(desc="Question the agent was answering.") token_budget: int = dspy.InputField(desc="Hard token budget for the context memory.") diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 42f8215..5f37cad 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -5,6 +5,7 @@ from codespy.agents.context_safe import ContextSafe from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, + ContextMemory, ItemTag, ) @@ -76,7 +77,17 @@ class DistillerSig(dspy.Signature): — that frames any question Medium value: - - Actions: tool execution action patterns that transfer across runs. + - Actions: tool execution patterns that transfer across runs. + Record what tool was used, for what purpose, and what the result + was. Focus on tool-use strategies that would save a future agent + exploration work. Do NOT record every individual tool call — only patterns + that a future run on the same context would benefit from. + - 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 @@ -109,8 +120,8 @@ class DistillerSig(dspy.Signature): """ trajectory: str = dspy.InputField(desc="The agent's full execution trajectory.") - context_memory: str = dspy.InputField( - desc="Current context memory (topic-grouped, with item IDs)." + context_memory: ContextMemory = dspy.InputField( + desc="Current context memory." ) question: str = dspy.InputField(desc="The question the agent was answering.") max_context_item_tokens: int = dspy.InputField( @@ -162,7 +173,7 @@ def __init__(self): def forward( self, trajectory: str, - context_memory: str, + context_memory: ContextMemory, question: str, max_context_item_tokens: int, ): diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 3357a36..1529883 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -29,8 +29,22 @@ class PRContext(BaseModel): ) pr_number: int = Field(description="PR number") pr_title: str = Field(description="PR title") + pr_url: str = Field(description="Full PR URL (e.g. https://github.com/owner/repo/pull/123)") + pr_description: str = Field(default="", description="PR body/description") summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") + def to_topic(self) -> "Topic": + """Build a Topic representing this PR. + + Returns: + Topic object with id as PR URL and description as "PR #N: Title" + """ + from codespy.agents.memory.hippocampus.context_memory import Topic + return Topic( + id=self.pr_url, + description=f"PR #{self.pr_number}: {self.pr_title}"[:500], + ) + class IssueSeverity(StrEnum): """Severity level of an issue.""" @@ -114,9 +128,7 @@ class ScopeResult(BaseModel): 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" - ) + language: str | None = Field(default=None, description="Primary language detected") package_manifest: PackageManifest | None = Field( default=None, description="Package manifest info if present" diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 4783811..7121293 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -61,7 +61,7 @@ def _call_auditor( all_issues: list[Issue], run_id: str | None, scopes: list["ScopeResult"] | None, - topic_ids: list[str] | None, + topics: list["ScopeResult"] | None = None, ) -> dspy.Prediction: """Execute the auditor predictor (with or without Hippocampus memory).""" question = ( @@ -93,7 +93,7 @@ def _call_auditor( task_name="audit", run_id=run_id, initial_memory=initial_memory, - topic_ids=topic_ids, + topics=topics, ) result = mem( pr_title=review_context.pr_context.pr_title, @@ -137,7 +137,7 @@ def forward( all_issues: Sequence[Issue], run_id: str | None = None, scopes: list["ScopeResult"] | None = None, - topic_ids: list[str] | None = None, + topics: list["ScopeResult"] | None = None, ) -> tuple[str, str]: """Assess quality and recommend action. @@ -147,7 +147,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 + topics: Optional list of Topic objects for auto-tagging Returns: Tuple of (quality_assessment, recommendation) @@ -177,7 +177,7 @@ def forward( list(all_issues), run_id, scopes, - topic_ids, + topics, ) 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 4a9ed97..651ebef 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -308,7 +308,8 @@ async def _review_scope( f"{review_context.pr_context.pr_title}: " f"{review_context.pr_context.summary}" ) - topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] + pr_ctx = review_context.pr_context + topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] mem = Hippocampus( agent, budget=self._settings.get_memory_budget("code_review"), @@ -317,7 +318,7 @@ async def _review_scope( task_name="code_review", run_id=run_id, initial_memory=scope_initial_memory, - topic_ids=topic_ids, + topics=topics, ) 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 46083c2..2095090 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -230,7 +230,8 @@ async def _review_scope( f"{review_context.pr_context.pr_title}: " f"{review_context.pr_context.summary}" ) - topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] + pr_ctx = review_context.pr_context + topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] mem = Hippocampus( reviewer, budget=self._settings.get_memory_budget("doc"), @@ -239,7 +240,7 @@ async def _review_scope( task_name="doc", run_id=run_id, initial_memory=scope_initial_memory, - topic_ids=topic_ids, + topics=topics, ) result = await mem.aforward( patches=patches, diff --git a/src/codespy/agents/reviewer/modules/helpers.py b/src/codespy/agents/reviewer/modules/helpers.py index ca86c31..620c766 100644 --- a/src/codespy/agents/reviewer/modules/helpers.py +++ b/src/codespy/agents/reviewer/modules/helpers.py @@ -151,7 +151,6 @@ def make_scope_relative(scope: ScopeResult) -> ScopeResult: subroot=".", scope_type=scope.scope_type, has_changes=scope.has_changes, - is_dependency=scope.is_dependency, language=scope.language, package_manifest=manifest, changed_files=relative_files, diff --git a/src/codespy/agents/reviewer/modules/manifest_parser.py b/src/codespy/agents/reviewer/modules/manifest_parser.py index fb8ca33..21e2e19 100644 --- a/src/codespy/agents/reviewer/modules/manifest_parser.py +++ b/src/codespy/agents/reviewer/modules/manifest_parser.py @@ -8,584 +8,6 @@ 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, 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, 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, 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 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, 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, 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, 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, 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, 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 f8e52da..b4bb4e5 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -27,12 +27,7 @@ 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.agents.reviewer.modules.manifest_parser import extract_package_name from codespy.config import get_settings from codespy.config_memory import get_memory_store from codespy.tools.git.client import get_client @@ -1096,49 +1091,20 @@ async def _refine_scopes( if scope.subroot in boundary_descriptions: scope.description = boundary_descriptions[scope.subroot] - # Build topic IDs + internal lookup - internal_packages: dict[str, str] = {} + # Build topic IDs 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(pr.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 + # Build Topics scope_topics: list[Topic] = [] for scope in final_scopes: - 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, ) ) @@ -1157,13 +1123,18 @@ async def _refine_scopes( stamp_topic_ids = [scope_topics[0].id] else: stamp_topic_ids = [] - + # Add PR URL topic (provides description; not a scope) + pr_ctx = review_context.pr_context + scope_topics.append(Topic( + id=pr_ctx.pr_url, + description=f"PR #{pr_ctx.pr_number}: {pr_ctx.pr_title}"[:500], + )) # 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 for episode persistence if mem is not None and stamp_topic_ids: + stamp_topic_ids.append(pr_ctx.pr_url) mem._topic_ids = stamp_topic_ids mem.cmem.bind_topics(scope_topics, stamp_topic_ids) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 2822e62..f9d1a07 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -9,12 +9,13 @@ 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.context_memory import Topic 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 if TYPE_CHECKING: - from codespy.agents.reviewer.models import ScopeResult + from codespy.agents.reviewer.models import PRContext, ScopeResult logger = logging.getLogger(__name__) @@ -47,28 +48,22 @@ def __init__(self) -> None: def forward( self, - pr_title: str, - pr_description: str, - pr_number: int, + pr_context: "PRContext", changed_file_paths: list[str], patches: str, - repo_slug: str, run_id: str | None = None, scopes: list["ScopeResult"] | None = None, - topic_ids: list[str] | None = None, + topics: list[Topic] | None = None, ) -> str: """Generate a PR summary. Args: - pr_title: Title of the pull request - pr_description: Description/body of the PR - pr_number: PR number + pr_context: PRContext with PR identity (repo_slug, pr_number, pr_title, pr_description, pr_url) 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 scopes: List of resolved scopes for per-scope episode persistence - topic_ids: Optional list of topic IDs for auto-tagging + topics: Optional list of Topic objects for auto-tagging Returns: Summary string @@ -76,7 +71,7 @@ def forward( if not self._settings.is_signature_enabled("summary"): logger.debug("Skipping summary: disabled") - return pr_title or "No title" + return pr_context.pr_title or "No title" # Load latest "summary" episode per scope and merge initial_memory: ContextMemory | None = None @@ -110,7 +105,7 @@ def forward( ) logger.info("Generating PR summary...") - question = f"summarize {repo_slug}: pull request {pr_number} {pr_title}" + question = f"summarize {pr_context.repo_slug}: pull request {pr_context.pr_number} {pr_context.pr_title}" mem: Hippocampus | None = None with SignatureContext("summary", self._cost_tracker): @@ -123,17 +118,17 @@ def forward( task_name="summary", run_id=run_id, initial_memory=initial_memory, - topic_ids=topic_ids, + topics=topics, ) result = mem( - pr_title=pr_title, - pr_description=pr_description, + pr_title=pr_context.pr_title, + pr_description=pr_context.pr_description, changed_file_paths=changed_file_paths, patches=patches, ) # Fire-and-forget episode save _store = get_memory_store(self._settings) - _common_dir = _deepest_common_folder(scopes, repo_slug) if scopes else f"/{repo_slug}/" + _common_dir = _deepest_common_folder(scopes, pr_context.repo_slug) if scopes else f"/{pr_context.repo_slug}/" _summary_text = result.summary _scopes = scopes def _persist(): @@ -147,8 +142,8 @@ def _persist(): submit_episode_save(_persist, name="summary-episode-save") else: result = summarizer( - pr_title=pr_title, - pr_description=pr_description, + pr_title=pr_context.pr_title, + pr_description=pr_context.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 9fcf374..ec84d3e 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -408,7 +408,8 @@ async def _review_scope( f"{review_context.pr_context.pr_title}: " f"{review_context.pr_context.summary}" ) - topic_ids = [scope.topic(pr.repo_full_name).id] if pr else [] + pr_ctx = review_context.pr_context + topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] mem = Hippocampus( supply_chain_agent, budget=self._settings.get_memory_budget("supply_chain"), @@ -417,7 +418,7 @@ async def _review_scope( task_name="supply_chain", run_id=run_id, initial_memory=scope_initial_memory, - topic_ids=topic_ids, + topics=topics, ) result = await mem.aforward( manifest_path=manifest_path, diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index e0518bb..6b77982 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -9,6 +9,7 @@ from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access +from codespy.agents.memory.hippocampus.context_memory import Topic from codespy.agents.reviewer.models import ( Issue, LocalReviewConfig, @@ -192,6 +193,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: repo_slug=pr.repo_slug, pr_number=pr.number, pr_title=pr.title, + pr_url=pr.url, + pr_description=pr.body or "", summary=pr.title, # Use title as placeholder since summary hasn't run ) metadata = ReviewMetadata(repo_path=repo_path, run_id=run_id, pr=pr, is_local=is_local) @@ -212,24 +215,24 @@ def forward(self, config: ReviewConfig) -> ReviewResult: # 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 pr.changed_files] patches = build_patches(pr.changed_files) - compact_patches(scopes, repo_path) + if self.settings.compact_patches: + logger.info("Compacting patches to function boundaries...") + compact_patches(scopes, repo_path) + else: + logger.debug("Compact patches disabled, using original PR patches") # 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(pr.repo_full_name).id for s in scopes] + # Build all scope topics (scope topics + PR topic) + all_scope_topics = [s.topic(pr.repo_full_name) for s in scopes] + all_scope_topics.append(pr_context.to_topic()) pr_summary = self.summarizer( - pr_title=pr.title, - pr_description=pr.body or "No description provided.", - pr_number=pr.number, + pr_context=pr_context, changed_file_paths=changed_file_paths, patches=patches, - repo_slug=pr.repo_slug, run_id=run_id, scopes=scopes, - topic_ids=all_scope_topic_ids, + topics=all_scope_topics, ) # Enrich review_ctx with actual summary pr_context.summary = pr_summary @@ -253,7 +256,7 @@ def forward(self, config: ReviewConfig) -> ReviewResult: all_issues=all_issues, run_id=run_id, scopes=scopes, - topic_ids=all_scope_topic_ids, + topics=all_scope_topics, ) # Collect per-signature statistics signature_stats_list = self._collect_signature_stats() diff --git a/src/codespy/config.py b/src/codespy/config.py index 65113c5..5b5eaea 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -143,6 +143,10 @@ class Settings(BaseSettings): # Enable provider-side prompt caching (Anthropic, OpenAI, Bedrock, etc.) enable_prompt_caching: bool = True + # Expand diff hunks to full function bodies (Tree-sitter) before review. + # When False (default), reviewers see original PR patches. + compact_patches: bool = False + # Minimum confidence threshold for reported issues. # Issues below this threshold are silently discarded by reviewer modules. min_confidence: float = Field(default=0.81, ge=0.0, le=1.0) diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 0ae456f..a28457e 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -85,7 +85,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 max_context_memory_tokens divided by - # max_context_item_tokens (16384 / 410 ~= 39 items). + # max_context_item_tokens (16384 / 512 = 32 items). # MEMORY_MAX_CONTEXT_MEMORY_TOKENS max_context_memory_tokens: int = Field(default=16384) @@ -95,7 +95,7 @@ class MemoryConfig(BaseModel): # than enforced in code (truncating an item could corrupt an exact constant). # The hard, memory-wide limit is max_context_memory_tokens, enforced by the # Evictor. MEMORY_MAX_CONTEXT_ITEM_TOKENS - max_context_item_tokens: int = Field(default=410) + max_context_item_tokens: int = Field(default=512) # 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 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..89389f3 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,57 @@ +"""Tests for Settings configuration. + +Tests for the Settings class in codespy.config, covering top-level +boolean configuration fields. +""" + +import os +import sys +from pathlib import Path + +import pytest + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from codespy.config import Settings + + +class TestCompactPatchesSetting: + """Tests for the compact_patches configuration setting.""" + + def test_default_is_false(self): + """Test that compact_patches defaults to False.""" + # Clear any env var that might affect the setting + env_backup = os.environ.pop("COMPACT_PATCHES", None) + try: + settings = Settings() + assert settings.compact_patches is False + finally: + # Restore env var if it was set + if env_backup is not None: + os.environ["COMPACT_PATCHES"] = env_backup + + def test_explicit_true(self): + """Test that compact_patches can be explicitly set to True.""" + settings = Settings(compact_patches=True) + assert settings.compact_patches is True + + def test_explicit_false(self): + """Test that compact_patches can be explicitly set to False.""" + settings = Settings(compact_patches=False) + assert settings.compact_patches is False + + def test_env_var_true(self, monkeypatch): + """Test that COMPACT_PATCHES env var sets the value to True.""" + monkeypatch.setenv("COMPACT_PATCHES", "true") + # Settings reload is needed to pick up env var changes + from codespy.config import reload_settings + settings = reload_settings() + assert settings.compact_patches is True + + def test_env_var_false(self, monkeypatch): + """Test that COMPACT_PATCHES env var sets the value to False.""" + monkeypatch.setenv("COMPACT_PATCHES", "false") + from codespy.config import reload_settings + settings = reload_settings() + assert settings.compact_patches is False diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index a8a4161..5451966 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -224,140 +224,6 @@ def test_gradle_from_settings_gradle(self, tmp_path: 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.""" @@ -481,30 +347,6 @@ 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.""" - 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 82ea34066ca678885a5527cde0a4c416590f6d64 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau <2980507+khezen@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:59:20 +0200 Subject: [PATCH 03/14] Fixes (#41) --- CHANGELOG.md | 8 +++ pyproject.toml | 2 +- src/codespy/__init__.py | 2 +- .../agents/reviewer/modules/auditor.py | 17 ++----- .../agents/reviewer/modules/scope_resolver.py | 49 +++++++++++++------ src/codespy/agents/reviewer/reviewer.py | 29 ----------- tests/test_scope_resolver.py | 11 ++++- 7 files changed, 57 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be7baf..87b612e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +## [1.0.16] - 2026-09-01 + +### Changed +- Manifest discovery (`_discover_manifests`) now scans only ancestor directories of changed files instead of walking the entire repository tree — significant performance improvement for large repos +- `_discover_manifests` signature: added `changed_files: list[ChangedFile]` parameter +- Auditor no longer receives per-file metadata: removed `changed_files` input from `AuditSignature` and `forward()` / `_call_auditor()` parameters +- `Auditor.forward()` signature simplified: `Sequence[ChangedFile]` parameter dropped, `all_issues` type narrowed from `Sequence[Issue]` to `list[Issue]` + ## [1.0.15] - 2026-09-01 ### Changed diff --git a/pyproject.toml b/pyproject.toml index 50c3628..839af9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.0.15" +version = "1.0.16" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index 47ad5af..7c3e0be 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.0.15" +__version__ = "1.0.16" diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 7121293..90ef985 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -1,7 +1,6 @@ """Auditor module — assesses code quality and provides recommendation after reviews.""" import logging -from collections.abc import Sequence from typing import TYPE_CHECKING import dspy @@ -13,7 +12,6 @@ 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 if TYPE_CHECKING: from codespy.agents.reviewer.models import ScopeResult @@ -34,9 +32,6 @@ class AuditSignature(dspy.Signature): 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" - ) all_issues: list[Issue] = dspy.InputField(desc="All issues found during review") quality_assessment: str = dspy.OutputField(desc="Overall assessment of code quality") @@ -55,9 +50,8 @@ def __init__(self) -> None: def _call_auditor( self, - auditor: dspy.ChainOfThought, + auditor: dspy.Module, review_context: ReviewContext, - audit_files: list[ChangedFile], all_issues: list[Issue], run_id: str | None, scopes: list["ScopeResult"] | None, @@ -98,7 +92,6 @@ def _call_auditor( result = mem( pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, - changed_files=audit_files, all_issues=all_issues, ) # Run episode save synchronously (auditor is the last module) @@ -124,7 +117,6 @@ def _call_auditor( result = auditor( pr_title=review_context.pr_context.pr_title, summary=review_context.pr_context.summary, - changed_files=audit_files, all_issues=all_issues, ) @@ -133,8 +125,7 @@ def _call_auditor( def forward( self, review_context: ReviewContext, - changed_files: Sequence[ChangedFile], - all_issues: Sequence[Issue], + all_issues: list[Issue], run_id: str | None = None, scopes: list["ScopeResult"] | None = None, topics: list["ScopeResult"] | None = None, @@ -143,7 +134,6 @@ def forward( Args: review_context: ReviewContext containing PR identity (memory loaded per-scope from prior audit episodes) - 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 @@ -173,8 +163,7 @@ def forward( result = self._call_auditor( auditor, review_context, - list(changed_files), - list(all_issues), + all_issues, run_id, scopes, topics, diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index b4bb4e5..d49a60a 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -624,7 +624,7 @@ def _resolve( Tuple of (active scopes, orphan files) """ excluded_dirs = self._settings.excluded_directories - manifests = self._discover_manifests(repo_path, excluded_dirs) + manifests = self._discover_manifests(repo_path, changed_files, excluded_dirs) logger.info( "Manifest discovery at %s found %d manifest(s): %s", repo_path, @@ -701,39 +701,60 @@ def _resolve( return active_scopes, orphans def _discover_manifests( - self, repo_path: Path, excluded_dirs: list[str] + self, repo_path: Path, changed_files: list[ChangedFile], excluded_dirs: list[str] ) -> dict[Path, tuple[str, str]]: - """Discover all package manifests in the repo. + """Discover package manifests in ancestor directories of changed files. Args: repo_path: Path to the repository root + changed_files: List of changed files to derive ancestor directories from excluded_dirs: List of directory names to exclude from scanning Returns: Dict mapping manifest directory -> (package manager, filename) """ - logger.debug("Walking %s for manifests (excluded: %s)", repo_path, excluded_dirs) + logger.debug("Scanning ancestor directories for manifests (excluded: %s)", excluded_dirs) 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(".")] + # Collect all unique ancestor directories from changed files + ancestor_dirs: set[Path] = set() + ancestor_dirs.add(Path(".")) # Always include root + + for changed_file in changed_files: + parts = changed_file.filename.split("/") + # Build each ancestor prefix from the path + for depth in range(1, len(parts)): + ancestor = Path("/".join(parts[:depth])) + ancestor_dirs.add(ancestor) + + # Scan each ancestor directory for manifests + for rel_dir in ancestor_dirs: + # Skip if any path component is in excluded_dirs or starts with . (but not root ".") + if rel_dir != Path("."): + dir_name = str(rel_dir) + if any(part in excluded_set or part.startswith(".") for part in dir_name.split("/")): + continue + + dir_path = repo_path / rel_dir + if not dir_path.is_dir(): + continue + + try: + files = os.listdir(dir_path) + except OSError: + continue 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 + manifests[rel_dir] = (MANIFEST_FILES[filename], filename) + break # 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) + manifests[rel_dir] = (pkg_mgr, filename) break return manifests diff --git a/src/codespy/agents/reviewer/reviewer.py b/src/codespy/agents/reviewer/reviewer.py index 6b77982..d8d1440 100644 --- a/src/codespy/agents/reviewer/reviewer.py +++ b/src/codespy/agents/reviewer/reviewer.py @@ -245,14 +245,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: ) logger.info(f"Found {len(all_issues)} issues") # Step 4: Run Audit (loads own prior episodes per scope, no memory inheritance 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(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, @@ -279,29 +273,6 @@ def forward(self, config: ReviewConfig) -> ReviewResult: signature_stats=signature_stats_list, ) - @staticmethod - def _collect_scoped_files(scopes: list) -> list[ChangedFile]: - """Collect de-duplicated changed files from identified scopes. - - The scope identifier already filters out binaries, vendor directories, - lock files, etc. This method collects only the in-scope files so the - summarizer operates on the same focused set as the review modules. - - Args: - scopes: Identified scopes from scope_resolver - - Returns: - De-duplicated list of ChangedFile objects from all scopes - """ - seen: set[str] = set() - scoped_files: list[ChangedFile] = [] - for scope in scopes: - for f in scope.changed_files: - if f.filename not in seen: - seen.add(f.filename) - scoped_files.append(f) - return scoped_files - def _collect_signature_stats(self) -> list[SignatureStatsResult]: """Collect statistics from all signatures that executed. diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index 6255d2f..58e62d0 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -72,8 +72,13 @@ def test_discover_manifests(self): (repo_path / "services" / "api").mkdir(parents=True) (repo_path / "services" / "api" / "go.mod").touch() + changed_files = [ + ChangedFile(filename="packages/auth/index.ts", status=FileStatus.MODIFIED), + ChangedFile(filename="services/api/main.go", status=FileStatus.MODIFIED), + ] + resolver = ScopeResolver() - manifests = resolver._discover_manifests(repo_path, []) + manifests = resolver._discover_manifests(repo_path, changed_files, []) assert len(manifests) == 2 assert Path("packages/auth") in manifests @@ -268,14 +273,16 @@ def test_root_does_not_suppress_when_nested_manifests_exist(self): (repo_path / "scripts" / "deploy").mkdir(parents=True) changed_files = [ + ChangedFile(filename="packages/auth/src/index.ts", status=FileStatus.MODIFIED), 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 + # Both packages/auth (manifest scope) and scripts/deploy (indicator scope) should exist scope_subroots = [s.subroot for s in scopes] + assert "packages/auth" in scope_subroots assert "scripts/deploy" in scope_subroots From 8160c67cf5cb19196c8932da733dce079302509a Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 8 Sep 2026 14:57:56 +0200 Subject: [PATCH 04/14] move episodic memory to postgres --- .env.example | 36 +- Dockerfile | 1 + action.yml | 35 +- codespy.yaml | 44 +- docs/configuration.md | 16 +- docs/memory.md | 47 +- poetry.lock | 1613 ++++++++--------- pyproject.toml | 2 + .../agents/memory/hippocampus/__init__.py | 2 - .../memory/hippocampus/context_memory.py | 24 +- .../agents/memory/hippocampus/episode.py | 134 +- .../agents/memory/hippocampus/hippocampus.py | 132 +- .../hippocampus/modules/cartographer.py | 11 +- src/codespy/agents/memory/pg0_manager.py | 275 +++ src/codespy/agents/memory/postgres.py | 746 ++++++++ src/codespy/agents/reviewer/models.py | 9 + .../agents/reviewer/modules/auditor.py | 56 +- .../agents/reviewer/modules/code_reviewer.py | 32 +- .../agents/reviewer/modules/doc_reviewer.py | 32 +- .../agents/reviewer/modules/scope_resolver.py | 30 +- .../agents/reviewer/modules/summarizer.py | 61 +- .../reviewer/modules/supply_chain_auditor.py | 32 +- src/codespy/config.py | 4 +- src/codespy/config_memory.py | 131 +- tests/test_config_memory.py | 288 +-- tests/test_context_memory.py | 49 +- tests/test_hippocampus.py | 448 ++++- 27 files changed, 2838 insertions(+), 1452 deletions(-) create mode 100644 src/codespy/agents/memory/pg0_manager.py create mode 100644 src/codespy/agents/memory/postgres.py diff --git a/.env.example b/.env.example index d78daed..42480dd 100644 --- a/.env.example +++ b/.env.example @@ -185,25 +185,25 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # ============================================================================= # Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) # 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. +# Episodes are stored in PostgreSQL using a relational schema with pgvector +# for semantic search. # -# Episodes are written to: -# global/episodic///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 +# Production uses MEMORY_POSTGRES_URI to connect to an external PostgreSQL +# instance (AWS RDS, etc.). For local development, pg0-embedded is used +# if MEMORY_POSTGRES_URI is not set. + +# PostgreSQL URI — set for production (AWS RDS, etc.) +# When unset, pg0-embedded auto-starts a local PostgreSQL instance. +# MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy + +# Bank ID — nickname, username, email, or agent name scoping all memory data. +# When unset, defaults to "codespy". +# MEMORY_BANK_ID=my-agent + +# pg0 embedded settings (local dev only, ignored when MEMORY_POSTGRES_URI is set) +# MEMORY_PG0_NAME=codespy +# MEMORY_PG0_PORT=5432 +# MEMORY_PG0_DATA_DIR=~/.cache/codespy/pg0 # default; persists on Docker-mounted codespy-cache volume # Reflection defaults — overridable per-signature via _MEMORY_* # MEMORY_DEFAULT_ENABLED=false diff --git a/Dockerfile b/Dockerfile index f5ad0eb..5e6091b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,7 @@ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ git \ ripgrep \ + libgssapi-krb5-2 \ && rm -rf /var/lib/apt/lists/* \ && useradd -m -u 1000 codespy diff --git a/action.yml b/action.yml index bf5bb60..ef833db 100644 --- a/action.yml +++ b/action.yml @@ -314,25 +314,12 @@ inputs: 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.' + memory-postgres-uri: + description: 'PostgreSQL connection URI for memory storage (e.g., postgresql://user:pass@host:5432/codespy). When unset, pg0-embedded auto-starts a local instance inside Docker.' 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)' + memory-bank-id: + description: 'Bank ID scoping all memory data (e.g., project name or agent name). Defaults to "codespy".' required: false memory-max-reflects: @@ -541,11 +528,8 @@ runs: # 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_POSTGRES_URI: ${{ inputs.memory-postgres-uri }} + MEMORY_BANK_ID: ${{ inputs.memory-bank-id }} MEMORY_DEFAULT_MAX_REFLECTS: ${{ inputs.memory-max-reflects }} MEMORY_DISTILLER_MODEL: ${{ inputs.memory-distiller-model }} MEMORY_DISTILLER_REASONING_EFFORT: ${{ inputs.memory-distiller-reasoning-effort }} @@ -649,11 +633,8 @@ runs: # 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_POSTGRES_URI" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_URI" + [ -n "$MEMORY_BANK_ID" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_BANK_ID" [ -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" diff --git a/codespy.yaml b/codespy.yaml index 3506792..ead5af0 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -74,36 +74,22 @@ gitlab: # default globally; enabled by default for summary — see `memory:` # blocks under each signature below. -# -# Episodes are written to: -# global/episodic///codespy--.json -# under `root` (filesystem) or `s3_bucket` (s3). +# Episodes are persisted in PostgreSQL. When no postgres_uri is set, +# pg0-embedded auto-starts a local PostgreSQL instance (zero config). +# For production, set postgres_uri to an external PostgreSQL (AWS RDS, etc.). memory: - backend: filesystem # MEMORY_BACKEND (filesystem | s3) - - # Filesystem backend - root: ~/.cache/codespy/memory # MEMORY_ROOT - - # S3 backend (used when backend: s3) - # --------------------------------------------------------------------------- - # 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) + # PostgreSQL connection URI for production (AWS RDS, etc.). + # When unset, pg0-embedded auto-starts a local PostgreSQL instance. + postgres_uri: null # MEMORY_POSTGRES_URI + + # Bank ID — scopes all memory data. Can be a nickname, username, email, + # or agent name. When unset, auto-generated from hostname. + bank_id: null # MEMORY_BANK_ID + + # pg0-embedded settings (local dev only, ignored when postgres_uri is set) + pg0_name: codespy # MEMORY_PG0_NAME + pg0_port: null # MEMORY_PG0_PORT (auto-detected if unset) + pg0_data_dir: null # MEMORY_PG0_DATA_DIR (default: ~/.cache/codespy/pg0) # Reflection defaults — overridable per-signature via signatures..memory default_enabled: false # MEMORY_DEFAULT_ENABLED diff --git a/docs/configuration.md b/docs/configuration.md index 14a0f75..4da5274 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -108,7 +108,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) | +| Max tokens | `DEFAULT_MAX_TOKENS` | `32000` | Output token budget per completion (reasoning tokens included) | | Temperature | `DEFAULT_TEMPERATURE` | `0.2` | Default temperature for LLM calls | | Max iterations | `DEFAULT_MAX_ITERS` | `5` | Maximum ReAct iterations for tool-using agents | | Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) | @@ -171,14 +171,16 @@ 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 | +| PostgreSQL URI | `MEMORY_POSTGRES_URI` | — | External PostgreSQL connection URI | +| Bank ID | `MEMORY_BANK_ID` | `codespy` | Scopes all memory data | +| pg0 name | `MEMORY_PG0_NAME` | `codespy` | pg0-embedded database name (local dev) | +| pg0 port | `MEMORY_PG0_PORT` | auto | pg0-embedded port (local dev) | | 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` | `16384` | Max tokens for persisted context memory | -| Item tokens | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | `512` | Soft per-item token limit | -| Trajectory tokens | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | `16384` | Cap on trajectory fed to Distiller | -| Question tokens | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | `8192` | Cap on serialized reflection inputs | +| Context memory tokens | `MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | `16384` | Ceiling on persisted context memory | +| Item tokens | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | `512` | Soft per-item token limit | +| Trajectory tokens | `MEMORY_MAX_TRAJECTORY_TOKENS` | `16384` | Cap on trajectory fed to Distiller | +| Question tokens | `MEMORY_MAX_QUESTION_TOKENS` | `8192` | Cap on serialized reflection inputs | See [Memory System](memory.md) for full memory configuration details. diff --git a/docs/memory.md b/docs/memory.md index ceccfe3..a3f2d1c 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -19,7 +19,8 @@ parsing schemas — and reuse it in subsequent reviews of the same code area. ### Episodes - An Episode captures one agent's run: task, context_memory, mutations, timestamp -- Stored as JSON at: `/global/episodic///codespy--.json` +- Stored in PostgreSQL (auto-created tables). pg0-embedded auto-starts + a local instance when no `MEMORY_POSTGRES_URI` is set. - `find_latest_episode()` loads the most recent episode by `modified_at` for a given path prefix ### Context Memory @@ -65,13 +66,12 @@ Item capacity ≈ context_memory_tokens / item_tokens (16384/512 = 32 items) | 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_POSTGRES_URI` | `memory.postgres_uri` | — | External PostgreSQL connection URI | +| `MEMORY_BANK_ID` | `memory.bank_id` | `codespy` | Scopes all memory data | +| `MEMORY_PG0_NAME` | `memory.pg0_name` | `codespy` | pg0-embedded database name | +| `MEMORY_PG0_PORT` | `memory.pg0_port` | auto | pg0-embedded port | | `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) | +| `MEMORY_DEFAULT_MAX_REFLECTS` | `memory.default_max_reflects` | `0` | Reflection iterations | ### Reflection Module LLM Overrides @@ -109,44 +109,27 @@ MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ``` +# Storage: pg0-embedded auto-starts when pg0-embedded is installed (default). +# For production, set MEMORY_POSTGRES_URI: +# MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy + ### 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 with external PostgreSQL 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-postgres-uri: ${{ secrets.MEMORY_POSTGRES_URI }} 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. +> **Note:** pg0-embedded is included in the Docker image. For persistent memory +> across CI runs, use an external PostgreSQL instance via `memory-postgres-uri`. --- diff --git a/poetry.lock b/poetry.lock index 3da8885..b5d7760 100644 --- a/poetry.lock +++ b/poetry.lock @@ -308,17 +308,17 @@ lxml = ["lxml"] [[package]] name = "boto3" -version = "1.43.72" +version = "1.43.86" description = "The AWS SDK for Python" optional = false python-versions = ">=3.10" files = [ - {file = "boto3-1.43.72-py3-none-any.whl", hash = "sha256:f1bbbad5ed8d8a8c64edb0cd092dc443c95a85623b2ac88b6f6d633717605f00"}, - {file = "boto3-1.43.72.tar.gz", hash = "sha256:6280ce03cc85e9110fd9fb7e2fbf11eae0b1177cb041a0d69aa88edc9d178cf9"}, + {file = "boto3-1.43.86-py3-none-any.whl", hash = "sha256:94543a5ce482df8bb6a0bf8a944bf5ccf6625889b1df0d7886dfc3311ebd4169"}, + {file = "boto3-1.43.86.tar.gz", hash = "sha256:aca7b5d7f31a90ad37bec773552ec9bfb57e0029c9aa0f0f4d51d5bf9607b1c4"}, ] [package.dependencies] -botocore = ">=1.43.72,<1.44.0" +botocore = ">=1.43.86,<1.44.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.19.0,<0.20.0" @@ -327,13 +327,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.43.72" +version = "1.43.86" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.10" files = [ - {file = "botocore-1.43.72-py3-none-any.whl", hash = "sha256:de5a1bcf8d7602c6cefc15016f15dad82981e339192531f31fa9483e11feea47"}, - {file = "botocore-1.43.72.tar.gz", hash = "sha256:1b878c69081e8e9d55aa4c0d85683e7b07f0e274a5554662f9507a46641be3d2"}, + {file = "botocore-1.43.86-py3-none-any.whl", hash = "sha256:4efc7fbd6e7616edbd55623bb585a044906149b9a5d3d8558420506526e5a5ac"}, + {file = "botocore-1.43.86.tar.gz", hash = "sha256:0e943c77ab6a54aaaf4d57e6026ede5c3dca341af71ce91f71849a6eae9d5dbf"}, ] [package.dependencies] @@ -344,154 +344,15 @@ urllib3 = ">=1.25.4,<2.2.0 || >2.2.0,<3" [package.extras] crt = ["awscrt (==0.36.0)"] -[[package]] -name = "brotli" -version = "1.2.0" -description = "Python bindings for the Brotli compression library" -optional = false -python-versions = "*" -files = [ - {file = "brotli-1.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8"}, - {file = "brotli-1.2.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a"}, - {file = "brotli-1.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92"}, - {file = "brotli-1.2.0-cp27-cp27m-win32.whl", hash = "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb"}, - {file = "brotli-1.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f"}, - {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f"}, - {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46"}, - {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e"}, - {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984"}, - {file = "brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de"}, - {file = "brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947"}, - {file = "brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2"}, - {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84"}, - {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d"}, - {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1"}, - {file = "brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997"}, - {file = "brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196"}, - {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744"}, - {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f"}, - {file = "brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd"}, - {file = "brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe"}, - {file = "brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a"}, - {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b"}, - {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3"}, - {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae"}, - {file = "brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03"}, - {file = "brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24"}, - {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84"}, - {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b"}, - {file = "brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d"}, - {file = "brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca"}, - {file = "brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f"}, - {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28"}, - {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7"}, - {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036"}, - {file = "brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161"}, - {file = "brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44"}, - {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab"}, - {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c"}, - {file = "brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f"}, - {file = "brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6"}, - {file = "brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c"}, - {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48"}, - {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18"}, - {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5"}, - {file = "brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a"}, - {file = "brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8"}, - {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21"}, - {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac"}, - {file = "brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e"}, - {file = "brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7"}, - {file = "brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63"}, - {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b"}, - {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361"}, - {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888"}, - {file = "brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d"}, - {file = "brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3"}, - {file = "brotli-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518"}, - {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69"}, - {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e"}, - {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7"}, - {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0"}, - {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea"}, - {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502"}, - {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64"}, - {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533"}, - {file = "brotli-1.2.0-cp36-cp36m-win32.whl", hash = "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96"}, - {file = "brotli-1.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13"}, - {file = "brotli-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8"}, - {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8"}, - {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc"}, - {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6"}, - {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190"}, - {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a"}, - {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12"}, - {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3"}, - {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a"}, - {file = "brotli-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982"}, - {file = "brotli-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16"}, - {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8"}, - {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990"}, - {file = "brotli-1.2.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526"}, - {file = "brotli-1.2.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2"}, - {file = "brotli-1.2.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675"}, - {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d"}, - {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5"}, - {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7"}, - {file = "brotli-1.2.0-cp38-cp38-win32.whl", hash = "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c"}, - {file = "brotli-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470"}, - {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1"}, - {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17"}, - {file = "brotli-1.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971"}, - {file = "brotli-1.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e"}, - {file = "brotli-1.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8"}, - {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a"}, - {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b"}, - {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4"}, - {file = "brotli-1.2.0-cp39-cp39-win32.whl", hash = "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49"}, - {file = "brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937"}, - {file = "brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a"}, -] - -[[package]] -name = "brotlicffi" -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.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] -cffi = [ - {version = ">=1.0.0", markers = "python_version < \"3.13\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.13\""}, -] - [[package]] name = "cachetools" -version = "7.1.7" +version = "7.1.8" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.10" files = [ - {file = "cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0"}, - {file = "cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50"}, + {file = "cachetools-7.1.8-py3-none-any.whl", hash = "sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b"}, + {file = "cachetools-7.1.8.tar.gz", hash = "sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43"}, ] [[package]] @@ -800,18 +661,15 @@ files = [ [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" files = [ - {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, - {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, + {file = "click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360"}, + {file = "click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "cloudpickle" version = "3.1.2" @@ -836,57 +694,57 @@ files = [ [[package]] name = "cryptography" -version = "50.0.0" +version = "50.0.1" 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.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"}, + {file = "cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527"}, + {file = "cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959"}, + {file = "cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b"}, + {file = "cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648"}, + {file = "cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6"}, + {file = "cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6"}, + {file = "cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149"}, + {file = "cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf"}, + {file = "cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80"}, + {file = "cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558"}, + {file = "cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e"}, + {file = "cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6"}, + {file = "cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b"}, + {file = "cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20"}, ] [package.dependencies] @@ -897,25 +755,23 @@ ssh = ["bcrypt (>=3.1.5)"] [[package]] name = "ddgs" -version = "9.15.0" +version = "9.16.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.15.0-py3-none-any.whl", hash = "sha256:2c6cce11d8625a030ed471265230dd6d1f5e12a6efecde5dfa3a1b33882888e0"}, - {file = "ddgs-9.15.0.tar.gz", hash = "sha256:12c4148da66525031214279d3ecb5170778a484f2616cac12667d0824818d07d"}, + {file = "ddgs-9.16.0-py3-none-any.whl", hash = "sha256:175d9198c958a263f51a06a54368ba0b41294942c0cd29f4aa71be03dbcd5f4a"}, + {file = "ddgs-9.16.0.tar.gz", hash = "sha256:161ca8e78ea08d40cd3f83fb12279b49322ffb342d981368bfa39bed9847d874"}, ] [package.dependencies] click = ">=8.1.8" -fake-useragent = ">=2.2.0" -httpx = {version = ">=0.28.1", extras = ["brotli", "http2", "socks"]} lxml = ">=4.9.4" -primp = ">=1.2.3" +primp = ">=1.3.1" [package.extras] 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"] +dev = ["lxml-stubs", "mypy (>=1.17.1)", "prek", "pytest (>=8.4.1)", "pytest-trio", "ruff (>=0.13.0)", "types-PyYAML", "types-Pygments", "types-pexpect", "types-ujson"] mcp = ["mcp (>=2.0)"] [[package]] @@ -942,13 +798,13 @@ files = [ [[package]] name = "dspy" -version = "3.3.0" +version = "3.3.1" 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.3.1-py3-none-any.whl", hash = "sha256:250049f565f52c014609ce2d3ca0de17a6c9449ac961492d61a009aa30dceabf"}, + {file = "dspy-3.3.1.tar.gz", hash = "sha256:ca53a428ac6a30984a894cf3847cf623b7dd7e8aadf8a63620b1f1f6155e4eaf"}, ] [package.dependencies] @@ -956,7 +812,7 @@ anyio = "*" cachetools = ">=5.5.0" cloudpickle = ">=3.1.2" diskcache = ">=5.6.0" -gepa = {version = "0.1.1", extras = ["dspy"]} +gepa = {version = "0.1.4", extras = ["dspy"]} json-repair = ">=0.54.2" litellm = ">=1.65.8" mcp = {version = "*", optional = true, markers = "python_version >= \"3.10\" and extra == \"mcp\""} @@ -970,7 +826,8 @@ tqdm = ">=4.66.1" [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)"] +deno = ["deno (>=2.4.5,<3.0.0)"] +dev = ["build (>=1.0.3)", "datamodel_code_generator (>=0.26.3)", "fastapi (<0.140.7)", "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"] @@ -979,17 +836,6 @@ 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" -version = "2.2.0" -description = "Up-to-date simple useragent faker with real world database" -optional = false -python-versions = ">=3.9" -files = [ - {file = "fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24"}, - {file = "fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2"}, -] - [[package]] name = "fastuuid" version = "0.14.0" @@ -1079,13 +925,13 @@ files = [ [[package]] name = "filelock" -version = "3.32.3" +version = "3.32.5" description = "A platform independent file lock." optional = false python-versions = ">=3.10" files = [ - {file = "filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09"}, - {file = "filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f"}, + {file = "filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2"}, + {file = "filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d"}, ] [[package]] @@ -1268,20 +1114,22 @@ tqdm = ["tqdm"] [[package]] name = "gepa" -version = "0.1.1" +version = "0.1.4" 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.1.4-py3-none-any.whl", hash = "sha256:12b971039599625c156d2231f6d72a29c31a22e9c237689459b5f1a3c353f532"}, + {file = "gepa-0.1.4.tar.gz", hash = "sha256:6dd153a676ae5481764860d19286a9c0e8ddb5ef70d7f13044faf24978bdb6b8"}, ] [package.extras] -build = ["build", "packaging", "requests", "semver", "setuptools (>=77.0.1)", "twine", "wheel"] +build = ["build", "packaging", "requests (>=2.33.0)", "semver", "setuptools (>=77.0.1)", "twine", "wheel"] +confidence = ["litellm (>=1.64.0,<1.92)", "litellm (>=1.81.0,<1.92)", "llm-structured-confidence (>=0.4.5)"] 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)"] +full = ["cloudpickle (>=3.0.0)", "datasets (>=2.14.6)", "datasets (>=4.5.0)", "litellm (>=1.83.0,<1.92)", "mlflow (>=3.11.1)", "mlflow-skinny (>=3.11.1)", "pandas (>=2.3.3)", "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"] +langchain = ["langchain (>=1.0)", "langchain-core (>=1.0)", "tqdm (>=4.66)"] test = ["gepa[full]", "pyright", "pytest"] [[package]] @@ -1300,13 +1148,13 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.59" +version = "3.1.61" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c"}, - {file = "gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4"}, + {file = "gitpython-3.1.61-py3-none-any.whl", hash = "sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c"}, + {file = "gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95"}, ] [package.dependencies] @@ -1327,21 +1175,6 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] -[[package]] -name = "h2" -version = "4.4.1" -description = "Pure-Python HTTP/2 protocol implementation" -optional = false -python-versions = ">=3.10" -files = [ - {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.2,<5" -hyperframe = ">=6.1,<7" - [[package]] name = "hf-xet" version = "1.6.0" @@ -1371,17 +1204,6 @@ files = [ [package.extras] tests = ["pytest"] -[[package]] -name = "hpack" -version = "4.2.0" -description = "Pure-Python HPACK header encoding" -optional = false -python-versions = ">=3.10" -files = [ - {file = "hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986"}, - {file = "hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0"}, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -1416,13 +1238,9 @@ files = [ [package.dependencies] anyio = "*" -brotli = {version = "*", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"brotli\""} -brotlicffi = {version = "*", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"brotli\""} certifi = "*" -h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""} httpcore = "==1.*" idna = "*" -socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""} [package.extras] brotli = ["brotli", "brotlicffi"] @@ -1444,13 +1262,13 @@ files = [ [[package]] name = "huggingface-hub" -version = "1.27.0" +version = "1.29.0" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.10.0" files = [ - {file = "huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d"}, - {file = "huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df"}, + {file = "huggingface_hub-1.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd"}, + {file = "huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad"}, ] [package.dependencies] @@ -1470,37 +1288,26 @@ dev = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] gradio = ["gradio (>=5.0.0)", "requests"] hf-xet = ["hf-xet (>=1.5.2,<2.0.0)"] -mcp = ["mcp (>=1.8.0)"] +mcp = ["mcp (>=1.8.0,<2.0.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)", "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)"] -[[package]] -name = "hyperframe" -version = "6.1.0" -description = "Pure-Python HTTP/2 framing" -optional = false -python-versions = ">=3.9" -files = [ - {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, - {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, -] - [[package]] name = "idna" -version = "3.18" +version = "3.19" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" files = [ - {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, - {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, + {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, + {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, ] [package.extras] -all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] [[package]] name = "importlib-metadata" @@ -1680,13 +1487,13 @@ files = [ [[package]] name = "json-repair" -version = "0.63.2" +version = "0.63.4" description = "A package to repair broken json strings" optional = false python-versions = ">=3.10" files = [ - {file = "json_repair-0.63.2-py3-none-any.whl", hash = "sha256:7354c2dd433bf15dedf98d2932ae9cd1ea197d7edc77e6c0538f08e34616580e"}, - {file = "json_repair-0.63.2.tar.gz", hash = "sha256:8385ca04afbf411eebd9f0ba064155d1afe37442aaad52f693825c65cc314586"}, + {file = "json_repair-0.63.4-py3-none-any.whl", hash = "sha256:0f374f3eee21454aef0a5d72c06b8689b660a1788f80ab392639e3f7d5c5d458"}, + {file = "json_repair-0.63.4.tar.gz", hash = "sha256:77aa642193d62b02b889e8ce0df33898d3ea87282f0b9d8653f8ce8772c642b4"}, ] [package.extras] @@ -1875,23 +1682,24 @@ files = [ [[package]] name = "litellm" -version = "1.97.0" +version = "1.99.0" description = "Library to easily interface with LLM API providers" optional = false python-versions = "<3.15,>=3.10" files = [ - {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"}, + {file = "litellm-1.99.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a43e8716da8beed04480e91b4233ff2f1ab1fedd84cad332dbc526b54a9229ca"}, + {file = "litellm-1.99.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e2b383070656fdbec4bc44602edaaed2a21e99ceee4ea0a4650c8cb381e67b59"}, + {file = "litellm-1.99.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e461b7ce53af990e5287cf7ae30d82c956b56dcce886f63ef39a76e678f82a3b"}, + {file = "litellm-1.99.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1c45097e426fed2ae7fbd38b5404c3addeb203d0e1148c0a59848aabd5fe83c6"}, + {file = "litellm-1.99.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e42f94731665b68e263481efd79f7629e9b97eed7e10c57dc37a890eca058227"}, + {file = "litellm-1.99.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71109c323164b4b6776ff259876523e6e883a465aa1413dd51a1bda8e92efc5f"}, + {file = "litellm-1.99.0-cp310-abi3-win_amd64.whl", hash = "sha256:5617804e838499bce8fecb41ad9bc984b7977361e557666fed0fef0c4623ce62"}, + {file = "litellm-1.99.0.tar.gz", hash = "sha256:594bf4b6ff6b79c6aa3c3b78c0e939d4afd12687e076ba3fb608d38a5aa7f9c6"}, ] [package.dependencies] aiohttp = ">=3.14.2,<4.0" +boto3 = ">=1.43.1,<2.0" click = ">=8.0.0,<9.0" fastuuid = ">=0.14.0,<1.0" httpx = ">=0.28.0,<1.0" @@ -1908,12 +1716,12 @@ tokenizers = ">=0.21.0,<1.0" [package.extras] 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)"] +cli = ["inquirerpy (>=0.3.4,<1.0)", "keyring (>=25.6.0,<26.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 = ["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.59)", "litellm-proxy-extras (==0.4.89)", "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)"] @@ -1922,145 +1730,187 @@ utils = ["numpydoc (>=1.8.0,<2.0)"] [[package]] name = "lxml" -version = "6.1.1" +version = "6.1.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "lxml-6.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:522387e05cd015a81d1dc621fb167fb42b8f629ccd2e8b39de583828f165aae6"}, + {file = "lxml-6.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d86130d70a2557cdf825dffc56255f1f16b83a7bbeab677b4cd040c4c53d8c52"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08cd52e6487435c75f2da0a5b276beef7fed161681b93ab766e66b954f0c349a"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:785761d5123f222cd97f2263a510107226fe32ce7aa7824a90616a41c574ace1"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae520f189895c5dd7eeb2b7a372d464da6f4a1ba1d0ecb741b1d4fe4c1f699ac"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83e7510a6dda8df41d1b68b783de2953b3feb55a11dcebf693201ebaa5cc0c4a"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:c20fa05d128c463209ef5323ebf33ee1cac6d87cdc3933fd789fd3c101017c8e"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:e7269cc410f3cdf84a66914fc0ef54b1618115c87fb4f9a59a05c5dfc23bece1"}, + {file = "lxml-6.1.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7233a987a101bdf79059014130262a01339094a0a709f175162542f33b55d4e"}, + {file = "lxml-6.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee23f6599682bd4d48bb757c0633e78774eedfb65a7e52851f9ad182eeeb625e"}, + {file = "lxml-6.1.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e062f5ac1255dfa6c98e3e3863ec18bc79d0947d22d08921a3ca60cee40559fd"}, + {file = "lxml-6.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:cb0cf498efa3204621b3c5576f0accd80ad2ee85575f1cae5d2f98de32c8d9cc"}, + {file = "lxml-6.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ee7410c98222070fd717ad881ee2a80cc11826b7001b9a5a807155d8918bfc7a"}, + {file = "lxml-6.1.2-cp310-cp310-win32.whl", hash = "sha256:aa224ecc613d411690aa650dbf01daafbe385cd6c67145e80bc5fc01b3a71469"}, + {file = "lxml-6.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1c0173595dc1c25768f42681a1517dcfc74bb18a34695f127931cbd05f4dead6"}, + {file = "lxml-6.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:adbecbfe44a497c742792457b1c27300617967c18c3934d2416023eba8d8c553"}, + {file = "lxml-6.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da6a4f55f0e3308c07354b1ee239c5550afc212f81629a6067db505ace3b667a"}, + {file = "lxml-6.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f4d2c36fd5997d30ff19c29fb93293401d0daaf87512297d47610e6883964b5"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1d55a614d2f0457b1f7511c1b7bec0db0dcdd4af4d09d226829eb054c647527c"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:575fef7f30048b744dffb3e4ff64a18cac7dba3fd26efdea5730ade9d1bdeb33"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79b428c3242e63bdacf3b526a34e0b8b26583846fc597da84b8f0c3d5ea446b2"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12ecfea07d767f6accbf30b014e1c477b5eabb13eb4e8c748215efb52c0e314a"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:bfcbee8ffff4188f4c6d97eceeff36d8eb983cf838933cbc12ce5f5dd51476c6"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:822d9397033edbe530a13bb1e0091c0e817536b6aba87a9b4ad626ed779ca0bd"}, + {file = "lxml-6.1.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4303f904fb6c41b58dc70743b1d8a470aba6c9897427c48324cff1a95673ddb4"}, + {file = "lxml-6.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdd35422de747237f451e821766e2b6be3dd2c31955c1ecd7f17984c5b9bb62d"}, + {file = "lxml-6.1.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b3ca02ef3b5920b88119c82eb6badfb2d082b1f681d528a856dcce17c8706da8"}, + {file = "lxml-6.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4bf14db2f0214003ec7f46c4300e2065668fc93e20448c1c95bac2e952072168"}, + {file = "lxml-6.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2afd1688e372d8eafaa6f56c589399e0a87d086a0c110f6346b0b50f42e67e25"}, + {file = "lxml-6.1.2-cp311-cp311-win32.whl", hash = "sha256:aea814342f6afd20d832937ff8b333cd6506428a39c0c4c70c2380aab1887bfb"}, + {file = "lxml-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:b3db5497af55f7a557c95265dd3b91c75dc56364a7b59f258c45fa5576dce058"}, + {file = "lxml-6.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:e8dc3d29f2ed2bbf24c205a86326d6681230ace55abfb3f9d5230f42078ad63d"}, + {file = "lxml-6.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7d506bdba580ecb1a6ad2e2b5c49445e66d3e1f95894885739094393a1aad237"}, + {file = "lxml-6.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12acd337d2821cb8b9247dfe4b7aa2f2769a3df5ae8511b7e550df42b8f4d3c3"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5078ff51e6316c0f75ea8127c2cd24374747fb351f62fb93d1761f8ae5a04a40"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9477e14217c212e6023c994a71a1a349db19b0e10fd5bf189666b281ae63b1fd"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261d98065326676d7253882db0198d0aa06748d7ee0443367acf10b148273f99"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0666943ee1576fa890a6dc6316ef42e8241b5dd56f67bc5475acb2ac298c6ca9"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04cf9e3f4ee9cab9d9ba05401bef8668840fa9620fcd4d8e85a2d2fd0b0fa960"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:9429d2371d406344ed1da5b5686d9412e74137c07b0171278368ff704f470ed5"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:eff128ffdc093cc6317955934ad9751105d37ed8dbca3ff4ccd751af6be37185"}, + {file = "lxml-6.1.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ba58574d710b82ead7cbedea01cac3e110bc3ef82d4731519b74a2c11f7cf5e9"}, + {file = "lxml-6.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52f6d4dff133c9778a24e9a2cfc1608930b15869866171aacc5131b5a418a003"}, + {file = "lxml-6.1.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8807998c1023d1e9d60e02500f90e85a0752dbc0b670989806bba87b82dd5b42"}, + {file = "lxml-6.1.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2170d0a280c877b6e2dc6738217db947be35dd8cf09ca458b355aa1bab2a9e70"}, + {file = "lxml-6.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c67f3c1278f942e97d8665c2a690324aaea5137de16f056583a21f0ac706177f"}, + {file = "lxml-6.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093fbf547d0f3ca02705381f795a050fbb58988be4aac7f79f99f280c4082313"}, + {file = "lxml-6.1.2-cp312-cp312-win32.whl", hash = "sha256:be365ce8d2d411cf2fb573747684b4fd470fa6224e0094d9d5a21155acc369d3"}, + {file = "lxml-6.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:b97153ca609b434b712ddfb92cd6af101a7045a7724c542258bd4727a344472f"}, + {file = "lxml-6.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:7feb72424f19a893ae4f3373c7aae821b1aacb6076b708915c651f0683a97c49"}, + {file = "lxml-6.1.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:351318f5c0eb7fcab5b4fdb507c6f88fb2c4b5e67784c7e5911448c91fffb5d4"}, + {file = "lxml-6.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0edde95e4b4278dcc0175eda06dc8aa2631ad9f83ae5dbdbc4f0925e200b0b0"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8326e24ae6c3a6bfb03fa8b4793f9a5d804c125228aa067f652b0428e31b87c"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c534ed898413f439b048130011e99a4245ee13d62d431f6b4f7f2484d02a93a"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e37fe49fe2d5aa40a2cb1cc8176673ad7de0d124e6f4a509d9318f5979c7871"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9b52ea73a37fc64aa3357ff8607801d46dd170506d3cf8253a91a1d91639d4f9"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8b9a92652e75e7731309ea51db5dee892eef414ce70a6ec3441e5d36bf5189f"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:9088da25ecd609965f838d89fda0465a905b48f4dd90331db9845518f2177372"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:0349321a0537d4fdbebb2af06dd1b64676132c72e2ae250de8cdb58f8c43019c"}, + {file = "lxml-6.1.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b20440e578d269c5e8a722ab602ddd0f0cedb8b080006b3f936da9991a593d3b"}, + {file = "lxml-6.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7766e525282dd38fd89567311323e441996eb958e8e816d16b38f782e3aecd2a"}, + {file = "lxml-6.1.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9221442682c27417f10fe11184ea4cce174b25ab52465570b1f3ee3f85f320fa"}, + {file = "lxml-6.1.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75530642d8471327e691ab9b0513a5f9c77f38871014ceda40f51bb51765c0a1"}, + {file = "lxml-6.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:678e35f1cbca98f55107511ee21a60568535c950f3c2371819bd64504c980d20"}, + {file = "lxml-6.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c2bae42b3a09f977330a08f4a8fe72aec58c4bdb89069d3fe7272a71d885881"}, + {file = "lxml-6.1.2-cp313-cp313-win32.whl", hash = "sha256:5848f3de6a8de8a93cff9f068134393ff5fa69ac2a04399f7d49cd67c61c348c"}, + {file = "lxml-6.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:6cb0c87421946030b92b558be416852780a912454e3dcba0998e4497c9c588d5"}, + {file = "lxml-6.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:648861c19b775b89ebefa14586f85090b10163367476d77f242c4131c835ce73"}, + {file = "lxml-6.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d50a44113fe6800dcc8a859332b823a4735b1e6ae1b0063882e4cca569ec3e29"}, + {file = "lxml-6.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fa813b0247d0543a563b993ac3dba6168eef59e3a61448432cf5453300c2412b"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d858e718b94033ab4b67e4a58fe3114c65bae01ae2314a62fb39ae8897ed4324"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e3b666f57a5d81562f38c766c762416b0f6eb58a00590546911514b48412abd"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26ff164c6629e5c4d11c9e55d5ea3d6eed0be2a420eee1f55cbce6e2c23e231a"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:962c12b51d0b164f12569af225dea57568477e24a845b96eaccbef6c07e4cc03"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47e367dfe341521426692819803e260d0673899c0ff611f14af978d725e2c999"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:92c2b366028ac01e90399e6d17734ce6e4f4aeddd8ba75fbaf80ea11d6c6d645"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:7e81fc065ede5d58dd0bf0912025aee1bd04c52c2affd61fdb93226a97ce2fc6"}, + {file = "lxml-6.1.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:633ac039cb32366dd5935868e041e385875c017b8cd54ea56aeee3fe29ca5935"}, + {file = "lxml-6.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f3194777c0d05945ac91d8594be25d2679d1d826e01e1fc90bae568ff3a547b"}, + {file = "lxml-6.1.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1133bd969f2bfcc6b0c0cf7cdf5f2631e62b23fa2471ee8bd44f6ab73554ee9a"}, + {file = "lxml-6.1.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1edca8f4a92b94e873093df959f141d388f2141fcad0c47598442fb4730ef57a"}, + {file = "lxml-6.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8512b3775d68994dd1d6d533161e0a214f2ad9c634659d34a99c98e86c6c3d68"}, + {file = "lxml-6.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5005c0c9e4d749a76a2ff8bd5918a8bb248df8e08e73a55654b9f79c9cd1e2b"}, + {file = "lxml-6.1.2-cp314-cp314-win32.whl", hash = "sha256:e17e2c30e27f56da5551e7a425888b45f013e940b99ab07d125a1c33f77a4605"}, + {file = "lxml-6.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:87e9673cd8a3445024fe38e7f91b55fa3428437eec9b7a7ff7d81979520c0d2d"}, + {file = "lxml-6.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:878e7c8ada8f92c52f13f35a2ab98ef0adf7fd0211d164fc2af589e4c3cfed63"}, + {file = "lxml-6.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:94162456ed0a64fb1c06915df5bd06af4675ae3966d6048fcb73b0906e0e0222"}, + {file = "lxml-6.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4b0fa7109b1d0bc1747d8241a0853e135eefb1c978685241b544c46937383efd"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:604f4778632588d7c000e7e19430639dc12fca58b5b6e99edffba7631725ef0e"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a096d6a5f96b776a5b020cb45c17c545effd2a3b6639e6fa97bc95537600923"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6454d184d556eaf4cb3d6f69e405d21602d6fdcf08b8d57796824275986c6595"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b68f2548259bb04e0b3d5df0c397abe8b0080f5e1ffe4019fb7a8bf01a9339e"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c9cc4b6532abe154dbdebb42aaba8d52c852919591e45067f5b7d46a0405e88"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:57188e441ab24f906bd5a5c14eb55363ab51aa6c0de549f3dd320043721cc118"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d0bfd719c254bbe60ea022cff0e6ffb799a6fa7d4d72852cebe0257957b32d68"}, + {file = "lxml-6.1.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be6f87cd224254a8f81324e34cc655508b83f1d70458a1a39857ad2aa9925852"}, + {file = "lxml-6.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:074a88f70a7360a4a0c5be5d898062cd26f898c25b459efb1bdd43ae700c5a1a"}, + {file = "lxml-6.1.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:9031f5f01452681abf39fdd65f84a70cb01a7572a1bbf570042e826b1232d07b"}, + {file = "lxml-6.1.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:cfeac14425fc7a6fca7864b774d4ee63547926158f4a18c67d77b2c9a948acf1"}, + {file = "lxml-6.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8ec111ff8067325f85c08aa9c2b26179ec0537bb89c003fde31127139f85f82d"}, + {file = "lxml-6.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48e912f37c99a297175ba955f55a47c0e1c834b506ef162e52a6e4fe276e6e45"}, + {file = "lxml-6.1.2-cp314-cp314t-win32.whl", hash = "sha256:7c444c3a6e8e75334879980eed96568f0e12064c8b1913424eac1805e976736b"}, + {file = "lxml-6.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7f35ba7667004ecdafebbe08da7c9fa06ee6195275bb7ef7a29ee1901e69519c"}, + {file = "lxml-6.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d117f39b28ab8a330a74abdbe61c2255b51973b238db25fd6c2448de1eb2a02d"}, + {file = "lxml-6.1.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:1e3c67b817867c484794d7fe0d73045d7d0c67460c78a0a1249a9e92266e6a0e"}, + {file = "lxml-6.1.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:d3e97ac4353cca3fbbfa829bc0c6a913771573d1c6d46932d4335c46f2b7796a"}, + {file = "lxml-6.1.2-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:827438bf6c8292d22a409bb7990d7cffce410f33e7664e46ca74d2ecc26975ef"}, + {file = "lxml-6.1.2-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c470d192e27f97842a068cf12a1c1296b20ca716c56a9249715c6654bc192d19"}, + {file = "lxml-6.1.2-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef0b8ba6e13597f681b2b4924ca9c4e8c88420bf0e21d9a9006c757f2fc39d1f"}, + {file = "lxml-6.1.2-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:65c32ddc5d0750129c7b119fb57d48192b76d334c21e6b690d19dfb06b34af79"}, + {file = "lxml-6.1.2-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0aa07065497f191ad26c4b587ce5dbb5a7105285a3789aafd0661750e8bac537"}, + {file = "lxml-6.1.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cde6b8db7d2e5135129eb5e74b7b44dd2053aa767cd5023541fccedddc262453"}, + {file = "lxml-6.1.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b28842b30c4bc2e6afe137d98a5d2071a62589471e76d053bea55b0e53298af9"}, + {file = "lxml-6.1.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:11f529062255209a421ae4de5b1bb36b2f0a2e1a700745e675a4bf4084d13c00"}, + {file = "lxml-6.1.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:f8b89b3be75a37509602b03f9cfa1a28298d4eed4625748148307aeb907901b7"}, + {file = "lxml-6.1.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1a2331da06dd55a8184985306eb2afd72d708283ce7e85d67bba77317b785060"}, + {file = "lxml-6.1.2-cp315-cp315-win32.whl", hash = "sha256:442766b326d9892585a64e8c6c4b5ab81d0e6c0538c9f0fc11a84dc101a5d97f"}, + {file = "lxml-6.1.2-cp315-cp315-win_amd64.whl", hash = "sha256:a7fd1dd6faa3df9dcd8f1765237362cd885ca62cdf77a7c5f5ea383ae5b6048b"}, + {file = "lxml-6.1.2-cp315-cp315-win_arm64.whl", hash = "sha256:054175250531a5fb102d485743ff16412279c93add12385b3b1c3d7b16d8deaa"}, + {file = "lxml-6.1.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:84a2a46b93b789d8acb44cfcb3d967ce9dbe29884ddb93fbb1a33f0e0c8fcd86"}, + {file = "lxml-6.1.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4aced3284e0353c798b060fe2c175eb81410e99b9a7e2ae6951be5333732b111"}, + {file = "lxml-6.1.2-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47c92dc5167de16e27ace8332454f12ba172dcab04f7a78a9eae14e2e41b6a41"}, + {file = "lxml-6.1.2-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:40366c23a938008a3bedfcfd80709b3a857c188b4d710b083e978ef5d2c1c715"}, + {file = "lxml-6.1.2-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c4c6dc1b2485aaa4adfb6ed754f90dddcb2b96a66bbebc9e1ac242b5ce5e818"}, + {file = "lxml-6.1.2-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:3a698fad6f122a9b3e2dc2fb598c1de7329c74a67c7a334c9109a440de2508e5"}, + {file = "lxml-6.1.2-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14879fa5eb2b793c040bbfcb62011aa3015c65d6c9875e063ea98ce2029d51fb"}, + {file = "lxml-6.1.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b631174cd2e4d9f8a94ef17f911c6ded10ede93b5e7860dee7bbf85961d321e9"}, + {file = "lxml-6.1.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:ceafa5e0536c62a5cd9f65327fa0b57d6f0b0e3435daf2c98a78d0dde7ecbae1"}, + {file = "lxml-6.1.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7c482e87cc86bed78a50462560675bc2c348ef72c47596f9b933346d5a8e920e"}, + {file = "lxml-6.1.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c0d2dde8a50520efc51644587f0fc4810e3af7d3e029d7af0be93bf39e2b5c"}, + {file = "lxml-6.1.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dd7ea3fa47154b9fff90591b961e41b3718bd7fcd5bc2d9bb47e9845c8ace088"}, + {file = "lxml-6.1.2-cp315-cp315t-win32.whl", hash = "sha256:87534cec6ea325435e4adf2326b0cf3110eee9a47abf73652eb155db639c08c6"}, + {file = "lxml-6.1.2-cp315-cp315t-win_amd64.whl", hash = "sha256:4e220a9c297e5d36895d489a08c9a3f1f6193b6414e702c5fb751e4a3767f8d0"}, + {file = "lxml-6.1.2-cp315-cp315t-win_arm64.whl", hash = "sha256:f16a407766bac51c65d605b06d900821751a79aa20e12185f273f14a17180e7b"}, + {file = "lxml-6.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aebcc6b184c935e1f7091c09124cfe5107b7c2253894ba23ad646828c17e4c3b"}, + {file = "lxml-6.1.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6330cf0ce83f6273ad8ad99bdd25d6ebb3863912f9ac717f96bc8942706e0e26"}, + {file = "lxml-6.1.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af6585a466cee2c5a524f7fffc591844bd604a29fdd9cade964f548512b5ef7e"}, + {file = "lxml-6.1.2-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:841630176c15fa5d3c5cd6f755435d3c5540a82e1dd2a7de1799401f92ee6d24"}, + {file = "lxml-6.1.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:215bb3cc4be015ccac3c7d4f25eb7b941f857fe5b02c0e3504cca61f7fb12455"}, + {file = "lxml-6.1.2-cp38-cp38-win32.whl", hash = "sha256:7c687fd8e558c7d169f6f1987b696f37824d3a097f291bffd0ab4a2ea2307dfb"}, + {file = "lxml-6.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:69df1856cb6c065e5bfd23adcc7408bfa6dcf32b0018373a99b0769bd86e2256"}, + {file = "lxml-6.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2dcc69e307e0916c7a0b552212010938d02a664d29b6bda75ab2bc5fa487c861"}, + {file = "lxml-6.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:243ecef7cb7415766dd742336cd5b8361a84c6f297e2773c865b783724cbbe74"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08f0c9ed7cded07c5e798b17c9c25bbba5d0650c8ff0a7f65f84c634966f0f10"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8ffb17ec0a8bae18b6628ae40b0896eb264dd285e39a0faa864965c00933b64c"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d78ba560f3dd404d87b1fcc89b2b382d638ea2998431a3b2e5cda0f3ba2da91"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2b7fe53abced1fe8bd984a9ab3c8c98bc093ec4f9f543089a8817a493818208"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:20134744db7abcbd5232214e767814ef64e5ab57a5b7df93a2bd68b74ef0a6c0"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:a02164a8cd3e2dc028918e51af844c934c7a24a0b8f4064368360aa14ad1aac4"}, + {file = "lxml-6.1.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec8d09f460fdeb65f9ead9b75941e312def4bcbb23e1f951b7def061eb99501d"}, + {file = "lxml-6.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1fcfe8481302e6dec07909914b8f3f9e1739ae1615209d4b9e7544325fb699c4"}, + {file = "lxml-6.1.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3be94d2464f19e42d8c39a299f356b12f2fd095c28793671eabfcd9db9c76987"}, + {file = "lxml-6.1.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:86d93dc3882c283e9aa2124d7d2b50c85579485216a2b3b7f91ba479e31a128f"}, + {file = "lxml-6.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8e613018a5ac66de7abaf1acaae0d7af37a5e1b9bf1ae190a1198b0fdb988ad8"}, + {file = "lxml-6.1.2-cp39-cp39-win32.whl", hash = "sha256:446f1f92c137e0cbb97eb7e932e15315c11a7c86974f43f15e68c9707ac6a9f6"}, + {file = "lxml-6.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:058c79e172926ef524fb3c7c6beea4b55e15886ac99cb0c139ecaac6b375f1e2"}, + {file = "lxml-6.1.2-cp39-cp39-win_arm64.whl", hash = "sha256:5295205fd57510c19a0e46385b516119f3a781d45c2672159bce02949238981a"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:feda2ef68c339987dfb370af3a4b785dbc40f925723fe2365e68e43c2640f85a"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9bdc2db9e04538f917bba0242920764dd740649d8df58700d6d687ead4429429"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a16457e330b7099aa5a8e8bfa5d53a33a1672a819fa656157e9e6dc433ac7a4"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:614d4c5a34556e369b86cfcc8d0cf71cd0759a3444a464a07a9427ab0f5e3a99"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18467b0e9f7f0bc477df69e99829a59ae17fb37d34e5f68399371c7c67be9002"}, + {file = "lxml-6.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:351855814dec4ad55ca5f24d0f4b1cdaca7927fe48023a2965351845f3b60cff"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4622c5616683faf63791b349e6c8dad7717412dc5f29f4febe7575f110609a86"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:733dfb492ec3dfef8350a5cc896e90d202c5171e791e1609e77563751d69a15d"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4618b20f43dc98b49569b1dc822176140ea0f2598d672a6989187ba49bcbfec1"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f93bc5e25992f5545709000d840c6cafdbd022781a7a0ed79d58a5633733a4e8"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:662432a6103e671d971e06e75ed146d9ff67f39d2c98c2f26613b6057f54eafc"}, + {file = "lxml-6.1.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ba0dfead73be5be9ad0b7fbf9f31ff29c1b1eae858816dfc8d85099d6e4af0d6"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:85690cfc8ed54c4292e36a08bcf984dde7957e653fd6d94f59184244bcc35843"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e92e4419cad18d60b14bf18b82152fbae67f4b1128be7d73b172df275554f5d9"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50ee0c360862f4152db835b456e38614f94b674bca2a47bc8de7171ee6ccbbb8"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:927f3e1d04dc0906265fc0416c13500363e42cd683bbb8d46911c79b73d26800"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f86e23ed610727a7f025ebbff788f22a7956d3f1b24a25bb1d9286fc7b7642b0"}, + {file = "lxml-6.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2374235206ec83d4827ad219c93c0f7366b93626eab85392c0ee7c8026649376"}, + {file = "lxml-6.1.2.tar.gz", hash = "sha256:1055241852f2b02068af4a625a5d32c087db193c12251928af2562ecd2239f18"}, ] [package.extras] @@ -2207,13 +2057,13 @@ files = [ [[package]] name = "mcp" -version = "1.29.0" +version = "1.29.1" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" files = [ - {file = "mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7"}, - {file = "mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36"}, + {file = "mcp-1.29.1-py3-none-any.whl", hash = "sha256:b6310eeb59153300c4ab8b9aec4c52f4819a2d6a8e429eb43d908bed7c783648"}, + {file = "mcp-1.29.1.tar.gz", hash = "sha256:1967ba4c315f7a375146209949f45950d18b0efd2f913d7cf3400bc723ee5f04"}, ] [package.dependencies] @@ -2619,6 +2469,24 @@ hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] +[[package]] +name = "pg0-embedded" +version = "0.15.1" +description = "Python API for pg0 - embedded PostgreSQL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pg0_embedded-0.15.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:b014d6bb49246ebe8ca2ab25503e366aeccfa87b89760ca7a7e701de8c148471"}, + {file = "pg0_embedded-0.15.1-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:3eef6291a7fda4332d760c0a417f2e1930cfb4fc80c1b98b10583b0f958cc3e1"}, + {file = "pg0_embedded-0.15.1-py3-none-manylinux_2_35_aarch64.whl", hash = "sha256:96e1b0867421e3553e34b39c7492b2301aff34f85eeef7a553d45601fd2f4d77"}, + {file = "pg0_embedded-0.15.1-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:24336e666bd3c954cb70fac2f93db1f928116aa92cbd0aba9320f789acfbb263"}, + {file = "pg0_embedded-0.15.1-py3-none-win_amd64.whl", hash = "sha256:35a245d29452915a314ecb655f33a35d8d881583d6c00373ef616a47a84c639f"}, + {file = "pg0_embedded-0.15.1.tar.gz", hash = "sha256:3fef3f250f722e49f0a762d0bcf4f776948a13c852dfae505c2e3ba512391cc5"}, +] + +[package.extras] +dev = ["pytest (>=8.0.0)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -2636,42 +2504,42 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "primp" -version = "1.3.1" +version = "2.0.0" description = "HTTP client that can impersonate web browsers" optional = false python-versions = ">=3.10" files = [ - {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"}, + {file = "primp-2.0.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5de7c1a2c3437394d77dca87841796a0cb704bf00e764dcbd2f7c073a11cc969"}, + {file = "primp-2.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c55f8ab86b7cc076b34ee0316ded6c30e9932ea010aeae3f8fe5cf30d9ecccf7"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd191d6f381eb814e1a7cb34b7eb5f47df81e73abdaecd8a99a8635a866357a3"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7c48b027246e09540ad645536d28966f618e09c6a4dff148db4c221ccf858d8"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9179d4d7839cf0f912700d201af239b1a87ca22225101e2a43d1a34c2a46c95"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d46cea402c5c0e70d65c237d946d41b46ef1a550fe3053ad6b66a3776decb759"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e8e16a3efc6a523da721fa56a7e4eab8ee62c36db910fb62782999398320d0a"}, + {file = "primp-2.0.0-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:e8d9354b078b8dc891d2546bd8e9109aa81f1351609262707f4c7cd2db5a7876"}, + {file = "primp-2.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:75960909d205a88e234d4e50fbd4c7c36d8235ef51b0c085da67bb171900cadd"}, + {file = "primp-2.0.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5ccc2f3b333254d80a63f291b6d3e122a3eebe445833995b136ed1b5ae5c66e7"}, + {file = "primp-2.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:2bd730ca8d86912924a3e4b775ad7befbe2115e26d0cf5b3c214d09dee80cfba"}, + {file = "primp-2.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:196896fa89c6b1eaa11331b14fe2cf539b9564b5e3398df8352c71ce30c46d84"}, + {file = "primp-2.0.0-cp310-abi3-win32.whl", hash = "sha256:0776fd400adcc940e47bb70896fb474f6607ffa545f5f7107ccda8d055fa02c9"}, + {file = "primp-2.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:7f205260fd41a0a409157deec95e6c4ec670476a79d9b4810d4c767ee7f9e49e"}, + {file = "primp-2.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:caf90ecc042a12b18414e4110ce767f4f1b0cfbc70310c5df39af948cb3eb516"}, + {file = "primp-2.0.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2ef2652eb2fd0072e474452663ddc790531f337dafed280a62a43a36f95d2c76"}, + {file = "primp-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fdc9abaf01b82e0e4b5ea925273bf690c31338c3bb7bc28f64420bf93f66647b"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b88af1baeaa70fb168a9cd8567cf40396b4d023d39895b4bab25b3e7dc6ddb20"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db6a9efabcef034918e0ee92a78c21c2ccd93bdb994550c49f3c0edf207a0bc4"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e820ed0573d67b5d495252dcf29e6dba14c279453c5c2134955eb2b0fd3d258"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:819f1480be4efc1d0dca46fac7b9a3ec40fb0b89bc11932165b5abc7872dae00"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c97a87afd27c490b5384433748bc664ecd358a8d0a688abf48716a6baf03050"}, + {file = "primp-2.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:ca4efea30417cf1119aac732ead82fc649af40978d0c989a9c280e901f6d0b3a"}, + {file = "primp-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0ea78a7a2fc5d48a825fa73182911b956bcb58940297cbd60958963ec30375d4"}, + {file = "primp-2.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0f309642cff4e9f966fb8f312bf19239df04c7698b1b4ae142285528d08c4ddd"}, + {file = "primp-2.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:422001e516110a3b2d44b106ac65d248c431fb98e1d6cad034e0a6ee8f1d6eb2"}, + {file = "primp-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1abcdcd5d0ef7b918bdd6e56ce07baeba5b2fd136e9df1fe9f9443c35a6af9ec"}, + {file = "primp-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:8756ae0df3dda65591ef478e73c588588a4a07919ee5c8d871fdd4e78fefb357"}, + {file = "primp-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a83bbfd798c1a9c2c7f1c8b02bbfb38f91fd63ac9072add967915e2463d68aa0"}, + {file = "primp-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5dfad2f926451a64bcc3a6d84e76593ec82840f77c5500d080e98387c7a018b8"}, + {file = "primp-2.0.0.tar.gz", hash = "sha256:714ec75081b7a84f63d83f966eb36649b9a8ba93625113b142400c317f6e83c5"}, ] [package.extras] @@ -2807,6 +2675,112 @@ files = [ {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, ] +[[package]] +name = "psycopg" +version = "3.3.5" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "psycopg-3.3.5-py3-none-any.whl", hash = "sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e"}, + {file = "psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.3.5", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.3.5)"] +c = ["psycopg-c (==3.3.5)"] +dev = ["ast-comments (>=1.1.2)", "black (>=26.1.0)", "codespell (>=2.2)", "cython-lint (>=0.21)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0,<9.0)", "mypy (>=2.1.0)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "types-shapely (>=2.0)", "wheel (>=0.37)"] +docs = ["Sphinx (>=9.1)", "furo (==2025.12.19)", "sphinx-autobuild (>=2025.8.25)", "sphinx-autodoc-typehints (>=3.10.2)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=2.1.0)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.3.5" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.10" +files = [ + {file = "psycopg_binary-3.3.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0faa2475ab254ad1b507430131cf7f7f0be927ffdc03c32ad3b33d2ef63f42"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0d8a4b7ae47f3381e2ded89891d2455b809f4afb7e5b58086844abb8cfa420ea"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04f64b39830887c2c737b522cbfd6ad215d65e67ebfff674aa4cf21c02af487b"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d06da67e9c687c6a6fdac9da4b17cbeb296ddd59bd01f6416ed4294bc57c5faf"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:972cc28e943746e71ede254a4dfd1fdfcdd6dcadd6f375703849859f09377f24"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:553b5443cbc94fdb9b0e31b62acdf615e0780982d6b13752a15eb3d6c0dfd0d2"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fd5b047c9fd887b767d063845413e405f5de8ce1dc7a9d0da0637b77b836b469"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4901e5b9a31c230211a1871263b6594373bacd770b14ad9b14d349716cb69cbe"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b981d25fc2dd13fa7328e40703ea8a03f3e9d855ec946431e96a23206d3b9fd"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39e70c8e3b5fad70e2970ea4cc502bf3b612018f128aa1c670f1fa78b9774543"}, + {file = "psycopg_binary-3.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:ae67072db949d0c094b747a8ec52ad0fa3c42b27842a5f746f3613d54dde3fba"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6bd84e4cf67930f26f015dec33f615472b9c5871d46408efe112dbc1bc021de"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fdbeb38c9b7ca8fa57a7bda3802bedb62f4494ad3dd46c7dd36dc3f77fd5093f"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:06de14ac978a2d53e864069fb5487075c6e3cfb0740f1bfd7017bc8b9942067f"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8b66353b20e79bf7ac0a80f03ae97f522ccbbf909d687eec62f112e56c0276d"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af5084124fb2fd16557073822519dfe8c389636a16adef661a4c0c3918733171"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1344fd57a19737554670e67aecabd4fb37cd7937f2925840009645d641117e5a"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2719fe19a4da752c4110cc767716d0a5bdb760d1153d89018bb7c9c61717bde5"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:893ce86a4b997f6ca1261a7826db2506727332a6ff66646fa7f024b39b5e630e"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a5e45e4bb68656253ce5c7a344c0a425c293581eba954d7fd7c2e4b2dc9f3038"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8af7c0454cdce235d4aedcb5528857468f1490202d25e24fc7af40e176d563"}, + {file = "psycopg_binary-3.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:7b443f943abfe35aa5a776630cea27c9348aa66659286cee0b99084332252080"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25105f9b46bdf2a30fcb67f56976ed66f6855941ae16bc024192609b917d493c"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0249c3e960cdee686000eb77169fb6590105c05bacc37e057ccdffdcd8e6ebde"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5698ab5941a4d138c30fef858588e651fe7d583280cd6e41832825ad9e747750"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2a61e8147902771df7efe14062a3c8736347850d0d8befcf048235752504f2e"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f7e1e45aad410e20de45df2b159df68ff6c8dbf47a3501f806c4489b27f4ad2b"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c09775c549b40b274206e1b043c5e5b5af39666e85c98382a30bd05d23ab677b"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cf0e5e63ee86098299c673992053d556c489ba9ae6aca6cb6e24d16a8e0b09e6"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c065531e8c1815276f50dbfa283e3a7f022671414cdda6fa9a16794dd53b28f9"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9853b832b7b916e02ef68e0d5403a7dab2d5c1ddfe94f22b1b155eb862622f"}, + {file = "psycopg_binary-3.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:35885e333020fc152d27bea1a494bef13b2e68f6fd92b6229015e93539152008"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16"}, + {file = "psycopg_binary-3.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f8b132c7243ef5f503f0b6f986bf16d38a51b0df1c6ba2577743f128be03e3"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0cac998b9b1e82dec853d2e53b3d34d56a525cf231f9441a636cfd5992929a9"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:479b96fd78149cfa10369dc53fbfb89ee729be13146b584a23dbc7e164c0cf1e"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f45d77e398542ce0937d9fa3cd9d84e9c5fc6b34c50a66404ae840bada312750"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98a388509306e5e08a4203253ac52846bc1b034e5cbd0ae6da1211593cc28594"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab39e2794b95af61a2ff69e33e5ab6ac5df36e9ffea9a3b18e38b2aaca8c5ad5"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9c071bf78e5c2e6efa40bc9089a954d7b41221347a72f35c6bf2d8c96e632f75"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:14fdfd65a96ecbd8b586d14546105641f4a6ac7cbe335c786830ea4de94bbe60"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8dbd694f3741dd4ac5bc60b70e17f7841aefb3f0f38cef4d2756de270e03af43"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14f432430fd9e1a9e7d9ab2fe14956c77f5d074ebdc556a1ad04e9a1bd3fca04"}, + {file = "psycopg_binary-3.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:df209e64674a34b41662c67fdc8b4e0ffd77d2136393790691d086a09f9a6cab"}, +] + +[[package]] +name = "psycopg-pool" +version = "3.3.1" +description = "Connection Pool for Psycopg" +optional = false +python-versions = ">=3.10" +files = [ + {file = "psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5"}, + {file = "psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c"}, +] + +[package.dependencies] +typing-extensions = ">=4.6" + +[package.extras] +test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + [[package]] name = "pycparser" version = "3.0" @@ -2820,18 +2794,18 @@ files = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, - {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.46.4" +pydantic-core = "2.46.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -2841,131 +2815,131 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" files = [ - {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"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, ] [package.dependencies] @@ -2996,13 +2970,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pygithub" -version = "2.9.1" +version = "2.10.0" description = "Use the full Github API v3" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "pygithub-2.9.1-py3-none-any.whl", hash = "sha256:2ec78fca30092d51a42d76f4ddb02131b6f0c666a35dfdf364cf302cdda115b9"}, - {file = "pygithub-2.9.1.tar.gz", hash = "sha256:59771d7ff63d54d427be2e7d0dad2208dfffc2b0a045fec959263787739b611c"}, + {file = "pygithub-2.10.0-py3-none-any.whl", hash = "sha256:192ada2a76e4afc7d6b37e500c9bfeba1731e6506697445a5ba1c4af8bf0b924"}, + {file = "pygithub-2.10.0.tar.gz", hash = "sha256:90ff24ef1cd1bd57124c2a3869cafee9d7b066909129ecdaba2c2d1903bc118d"}, ] [package.dependencies] @@ -3313,125 +3287,141 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.7.19" +version = "2026.9.3" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" files = [ - {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"}, + {file = "regex-2026.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d416ed4058ea38f69884f3db523b0414c0094d262dde63694aa254e32a433e6"}, + {file = "regex-2026.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:107319f437fc382e1e445d0abf8923db07f76e5fb3245ac5b09604e5dc8d4f63"}, + {file = "regex-2026.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05e9f7d16b42686fb38b1702071a7359469ba89e9d516e2ba5228e077dcac524"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39d8d5490ba4b8f26ef2aef72b775b1e7fc1a5ebaf28d11e637eb27b0b6a3048"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4381151a29a7b9307842ff444609c4b9a402775fbe9afb3ec3e34ec0396bbae"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0efb99024ad5ba9198ffa816156b3319c3164b5a8d1e35940d92fdc9158b8d9f"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ab7ad8b557d1f21e8b4a97a2c1a2ac82cf10c92d6a875c90f038ca9ed85dc5b"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2a36e654181ecb996241bae256d28f26b03d019d6ef65c61dcf44396ab07c548"}, + {file = "regex-2026.9.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d36e9097a1bc6eebe8858216ccd2326f561662de4e50c27533452491c1cf1685"}, + {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c407afee6ea4caa313e815814e76d8447fcd32f81e3331d7937c2d33ab80c1b"}, + {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:82c27460ff2ea683159a204db2f9ce0f9549dec7070aa0809fbc36226bac1816"}, + {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5e49096c90364317897b8d9fe97e2f86063dc0fc0bcd352017ea6fdd703ff25d"}, + {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:11f9b104fa7b23f736b50fe1980a1422eb4641865e82614a25413deabccae575"}, + {file = "regex-2026.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:92e06a5ecabd6e8352da58b9201ae241a1e7382c1419defa9d2f64ad4ea6abf2"}, + {file = "regex-2026.9.3-cp310-cp310-win32.whl", hash = "sha256:6ff5a98456194621f757d6226f88035c00ebbeab0a9095dc9b5a9ec5cc94b50b"}, + {file = "regex-2026.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:14f8969a92ed847f78e8eed81c6a0384a9e8058a56eb659f83ddb59879c2e4e3"}, + {file = "regex-2026.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:32a32d2664e602b20e4b9c11234cb32f2e985323e453b2b396ea4ae686d82052"}, + {file = "regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65"}, + {file = "regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4"}, + {file = "regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e"}, + {file = "regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328"}, + {file = "regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71"}, + {file = "regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e"}, + {file = "regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01"}, + {file = "regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141"}, + {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc"}, + {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b"}, + {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124"}, + {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f"}, + {file = "regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce"}, + {file = "regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420"}, + {file = "regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc"}, + {file = "regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8"}, + {file = "regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77"}, + {file = "regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596"}, + {file = "regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e"}, + {file = "regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687"}, + {file = "regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753"}, + {file = "regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151"}, + {file = "regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce"}, + {file = "regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c"}, + {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32"}, + {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14"}, + {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275"}, + {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089"}, + {file = "regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b"}, + {file = "regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3"}, + {file = "regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36"}, + {file = "regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136"}, + {file = "regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280"}, + {file = "regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b"}, + {file = "regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9"}, + {file = "regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08"}, + {file = "regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211"}, + {file = "regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c"}, + {file = "regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a"}, + {file = "regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28"}, + {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e"}, + {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90"}, + {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b"}, + {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9"}, + {file = "regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec"}, + {file = "regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20"}, + {file = "regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd"}, + {file = "regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f"}, + {file = "regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886"}, + {file = "regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6"}, + {file = "regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7"}, + {file = "regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944"}, + {file = "regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b"}, + {file = "regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8"}, + {file = "regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec"}, + {file = "regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69"}, + {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7"}, + {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c"}, + {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc"}, + {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69"}, + {file = "regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380"}, + {file = "regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd"}, + {file = "regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846"}, + {file = "regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16"}, + {file = "regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16"}, + {file = "regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d"}, + {file = "regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595"}, + {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17"}, + {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970"}, + {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3"}, + {file = "regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5"}, + {file = "regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181"}, + {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef"}, + {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae"}, + {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88"}, + {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177"}, + {file = "regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e"}, + {file = "regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f"}, + {file = "regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16"}, + {file = "regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5"}, + {file = "regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9"}, + {file = "regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361"}, + {file = "regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab"}, + {file = "regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5"}, + {file = "regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be"}, + {file = "regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e"}, + {file = "regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb"}, + {file = "regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f"}, + {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c"}, + {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27"}, + {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7"}, + {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253"}, + {file = "regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20"}, + {file = "regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae"}, + {file = "regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57"}, + {file = "regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950"}, + {file = "regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4"}, + {file = "regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff"}, + {file = "regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76"}, + {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96"}, + {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35"}, + {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa"}, + {file = "regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23"}, + {file = "regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec"}, + {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de"}, + {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f"}, + {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544"}, + {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092"}, + {file = "regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69"}, + {file = "regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a"}, + {file = "regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803"}, + {file = "regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2"}, + {file = "regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db"}, ] [[package]] @@ -3614,29 +3604,29 @@ files = [ [[package]] name = "ruff" -version = "0.16.3" +version = "0.16.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b"}, + {file = "ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3"}, + {file = "ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105"}, + {file = "ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29"}, + {file = "ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf"}, + {file = "ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91"}, + {file = "ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a"}, + {file = "ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef"}, + {file = "ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26"}, + {file = "ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f"}, + {file = "ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e"}, + {file = "ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b"}, ] [[package]] @@ -3700,17 +3690,6 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -[[package]] -name = "socksio" -version = "1.0.0" -description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." -optional = false -python-versions = ">=3.6" -files = [ - {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, - {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, -] - [[package]] name = "soupsieve" version = "2.9.2" @@ -3779,68 +3758,75 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tiktoken" -version = "0.13.0" +version = "0.14.0" description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" optional = false python-versions = ">=3.9" files = [ - {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"}, + {file = "tiktoken-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3b12e54f8bec91433e41aff65d8d1f209a4f678081163747079806e5361f6c91"}, + {file = "tiktoken-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:94f77b60a8ab23580db19ae822744c9716c1720020d2179ca5605112d12326f1"}, + {file = "tiktoken-0.14.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f3d6cf93fbe2e7117eb7bedca684216fbe328a41f0843ce34245451d8eb2df1c"}, + {file = "tiktoken-0.14.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:18a1b651c4b032004bf7b4f1713391a54b2a341a52c6e8a2b59acae9d16e13c7"}, + {file = "tiktoken-0.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d91d68353bd167fdf26467e5ff9e56aaa5f87d6410c0238608629e4dc0d33"}, + {file = "tiktoken-0.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10f31e63e40313f2e518d87f7086cfa44e45f64cc14d8ae14103b41220c30a14"}, + {file = "tiktoken-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:c6cb9896a82b9ee44e15ba0b5c8044072f2e4d48acaa704c8d3feeef5ad9487c"}, + {file = "tiktoken-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79"}, + {file = "tiktoken-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948"}, + {file = "tiktoken-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f"}, + {file = "tiktoken-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513"}, + {file = "tiktoken-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78"}, + {file = "tiktoken-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e"}, + {file = "tiktoken-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da"}, + {file = "tiktoken-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36"}, + {file = "tiktoken-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4"}, + {file = "tiktoken-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6"}, + {file = "tiktoken-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d"}, + {file = "tiktoken-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482"}, + {file = "tiktoken-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6"}, + {file = "tiktoken-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3"}, + {file = "tiktoken-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f"}, + {file = "tiktoken-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94"}, + {file = "tiktoken-0.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06"}, + {file = "tiktoken-0.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d"}, + {file = "tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010"}, + {file = "tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632"}, + {file = "tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1"}, + {file = "tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450"}, + {file = "tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b"}, + {file = "tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e"}, + {file = "tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42"}, + {file = "tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c"}, + {file = "tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771"}, + {file = "tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098"}, + {file = "tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438"}, + {file = "tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa"}, + {file = "tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037"}, + {file = "tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef"}, + {file = "tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a"}, + {file = "tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58"}, + {file = "tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0"}, + {file = "tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232"}, + {file = "tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695"}, + {file = "tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49"}, + {file = "tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4"}, + {file = "tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871"}, + {file = "tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f"}, + {file = "tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea"}, + {file = "tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890"}, + {file = "tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5"}, + {file = "tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae"}, + {file = "tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1"}, + {file = "tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89"}, + {file = "tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3"}, + {file = "tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9"}, + {file = "tiktoken-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2ec16eb585332c55d022d86354e209ddf27326b1ea3477585ab248e7776d3b1f"}, + {file = "tiktoken-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa428a559d5fd02ae619aacaace86c7474a1f2702d2c01fc828908dd60f20f7a"}, + {file = "tiktoken-0.14.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:7b7acbb7a4b8383707bce22ad3c162006478c27b56368acd3e1fcb1658a80425"}, + {file = "tiktoken-0.14.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c3093001ddce822b4587e6e94bf6de36a5f97b3f31de1c9fc8d4fda144c59ff4"}, + {file = "tiktoken-0.14.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a140e83317fef02faeeb78d9a8efac623887f2feaf0055c55dcdb2b17f0226ad"}, + {file = "tiktoken-0.14.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:50a7e5646cbac2a8f7c3e8c0934ffda1a4357ee9c44b652434b23c3ed54d0900"}, + {file = "tiktoken-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:447ada49af4898b5e992f0b5799d2f3af385921102c211947ce3fe960dd919da"}, + {file = "tiktoken-0.14.0.tar.gz", hash = "sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874"}, ] [package.dependencies] @@ -4165,13 +4151,13 @@ core = ["tree-sitter (>=0.23,<1.0)"] [[package]] name = "typer" -version = "0.27.1" +version = "0.27.2" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.10" files = [ - {file = "typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56"}, - {file = "typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df"}, + {file = "typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d"}, + {file = "typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945"}, ] [package.dependencies] @@ -4205,6 +4191,17 @@ files = [ [package.dependencies] typing-extensions = ">=4.15.0" +[[package]] +name = "tzdata" +version = "2026.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931"}, + {file = "tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415"}, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -4224,13 +4221,13 @@ zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "uvicorn" -version = "0.52.3" +version = "0.52.4" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" files = [ - {file = "uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c"}, - {file = "uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58"}, + {file = "uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1"}, + {file = "uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86"}, ] [package.dependencies] @@ -4380,4 +4377,4 @@ type = ["pytest-mypy (>=1.0.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "5279d4fc40257d42c24df466a135b281bf82a3b38a0d6f71b49bd88dc305fbf3" +content-hash = "885f769ac40c22e1d6298ee073d3383bd706df862f4c56d1d7300200c55c2040" diff --git a/pyproject.toml b/pyproject.toml index 839af9a..5d6d9df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,11 @@ tree-sitter-objc = ">=3.0" tree-sitter-rust = ">=0.23" tree-sitter-hcl = ">=1.2.0" mcp = ">=1.29.0,<2.0.0" +psycopg = {version = ">=3.1", extras = ["binary", "pool"]} beautifulsoup4 = ">=4.12.0" markdownify = ">=0.15.0" ddgs = ">=8.0.0" +pg0-embedded = ">=0.15" [tool.poetry.group.dev.dependencies] pytest = ">=8.0.0" diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index f80bfd1..7fd017c 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -14,7 +14,6 @@ ) from codespy.agents.memory.hippocampus.episode import ( Episode, - find_latest_episode, join_episode_saves, submit_episode_save, ) @@ -40,7 +39,6 @@ "SectionName", "Topic", "compute_common_ancestor_topic_id", - "find_latest_episode", "join_episode_saves", "make_topic_id", "submit_episode_save", diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 5183bd3..9d37f8c 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -71,6 +71,8 @@ def _missing_(cls, value: object) -> OpType | None: "reusable_results": "rr", } +_PREFIX_TO_SECTION: dict[str, str] = {v: k for k, v in _SECTION_PREFIX.items()} + class Topic(BaseModel): """A topic representing a scope in the repository. @@ -276,10 +278,24 @@ def apply( if replaced: break if not replaced: - logger.warning( - "REPLACE target %r not found in context memory; skipping", - op.item_id, - ) + # Infer section from item_id prefix; fall back to ADD + prefix = op.item_id.split("-", 1)[0] if "-" in op.item_id else "" + section_name = _PREFIX_TO_SECTION.get(prefix) + if section_name: + logger.info( + "REPLACE target %r not found; falling back to ADD in %s", + op.item_id, section_name, + ) + new_id = f"{prefix}-{uuid.uuid4().hex}" + new_item = Item(id=new_id, content=op.content, topic_ids=topic_ids or []) + cm.section(section_name).append(new_item) + new_ids.append(new_id) + else: + logger.warning( + "REPLACE item_id %r has no valid prefix — " + "likely a topic ID; 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/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 917a769..2d7b061 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -4,14 +4,13 @@ import logging import threading +import uuid from collections.abc import Callable from datetime import UTC, datetime 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 logger = logging.getLogger(__name__) @@ -74,6 +73,7 @@ class Episode(BaseModel): record of the memory it produced. Attributes: + id: Unique identifier for this episode (caller-provided UUID). 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. @@ -91,11 +91,11 @@ class Episode(BaseModel): 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``. + review run can be correlated. mutations: Ordered sequence of Cartographer mutations applied during this episode. """ + id: uuid.UUID = Field(description="Unique episode identifier (caller-provided)") run_id: str = Field( default="", description=( @@ -104,8 +104,7 @@ class Episode(BaseModel): ), ) timestamp: datetime = Field( - default_factory=lambda: datetime.now(UTC), - description="UTC time the episode was recorded", + description="UTC time the episode was recorded (caller-provided)", ) task: str = Field(description="Wrapped signature name (or module class name as fallback)") module: str = Field(description="Wrapped dspy.Module class name") @@ -124,126 +123,3 @@ class Episode(BaseModel): default_factory=list, description="Ordered sequence of Cartographer mutations applied during this episode", ) - - -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``. - - Args: - store: A ``FileSystem`` or ``S3Client`` instance. - path: Source path (relative to the store's root / bucket). - - Returns: - The loaded ``Episode``. - - 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 - return episode - - -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. - - 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_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). - - 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_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) - if not candidates: - return None - # Sort by modified_at descending; epoch fallback for entries without timestamp - _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, - ) - # 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/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 98ef7cd..cebc5c1 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -1,3 +1,5 @@ +"""Hippocampus memory module for context-aware agents.""" + from __future__ import annotations import asyncio @@ -5,6 +7,7 @@ import logging import uuid from datetime import UTC, datetime +from typing import TYPE_CHECKING import dspy @@ -23,13 +26,14 @@ Operation, OpType, Topic, + _PREFIX_TO_SECTION, ) 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 +from codespy.agents.memory.hippocampus.modules.cartographer import Cartographer + +if TYPE_CHECKING: + from codespy.agents.memory.postgres import EpisodeStore logger = logging.getLogger(__name__) @@ -236,9 +240,6 @@ 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 # Accumulated mutations across _distill() calls within the current episode. @@ -319,6 +320,7 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: empty dict when omitted. """ self.episode = Episode( + id=uuid.uuid4(), task=self._task_name, module=self._module_name, question=self._episode_question or "", @@ -334,32 +336,9 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: self._mutations.clear() self._distill_step = 0 - def episode_file_path(self, dir: str, index: int = 0) -> str: - """Build the full episode file path from a directory. - - Path format: ``orgs//episodic/.codespy/.`` - ``[.].--.json`` # noqa: E501 - - Args: - dir: Directory identifying where this episode belongs (e.g. a - scope's ``/{host}/{owner}/{repo}/{subroot}/`` path or - ``/{owner}/{repo}/{subroot}/`` without host). - index: Episode index for this scope/task combination. - """ - 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, - store: Storage | None = None, - dir: str | None = None, + store: EpisodeStore | None = None, artifacts: dict[str, str] | None = None, ) -> None: """Consolidate the buffered trajectories into the memory and record an Episode snapshot. @@ -374,16 +353,10 @@ def end_episode( containing the task/module identity and a deep-copy snapshot of the updated context memory. - If both ``store`` and ``dir`` are provided the episode is persisted - via ``save_episode()`` after consolidation, at - ``orgs//episodic/.codespy/.--.json``. ``store`` may be a - ``FileSystem`` or an ``S3Client`` instance. + If ``store`` is provided, the episode is persisted via ``store.save_episode()``. 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. + store: Optional ``EpisodeStore`` to persist the episode after consolidation. artifacts: Named output artifacts to attach to the recorded episode (e.g. ``{"review": ""}``). Agent-agnostic — any caller can attach whatever markdown/text output it @@ -397,14 +370,12 @@ def end_episode( if not has_content: 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) - self._episode_index += 1 + if store is not None: + store.save_episode(self.episode) async def aend_episode( self, - store: Storage | None = None, - dir: str | None = None, + store: EpisodeStore | None = None, artifacts: dict[str, str] | None = None, ) -> None: """Async counterpart of :meth:`end_episode`. @@ -414,10 +385,7 @@ async def aend_episode( 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. + store: Optional ``EpisodeStore`` to persist the episode after consolidation. artifacts: Named output artifacts to attach to the recorded episode (e.g. ``{"review": ""}``). """ @@ -426,60 +394,14 @@ async def aend_episode( if not has_content: 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) - await asyncio.to_thread(_save_episode, store, path, self.episode) - self._episode_index += 1 - - 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: - ValueError: If no episode has been consolidated yet (call - ``end_episode()`` first). - OSError: If the write fails. - """ - 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_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 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. - - Args: - store: Storage backend (``FileSystem`` or ``S3Client``). - path: Source path within the store. - - Raises: - FileNotFoundError: If the path does not exist. - OSError: If reading or parsing fails. - """ - ep = _load_episode(store, path) - self.episode = ep - self.cmem = ep.context_memory - self.scores = {} - self._episode_trajectories.clear() - self._episode_question = None - self._reflected_count = 0 - self._episode_index = 0 - self._mutations.clear() - self._distill_step = 0 + if store is not None: + await asyncio.to_thread(store.save_episode, self.episode) 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 self._mutations.clear() self._distill_step = 0 @@ -542,6 +464,24 @@ def _record_mutations( topic_ids=old_item.topic_ids, ) ) + else: + # Fallback REPLACE→ADD: mirrors apply()'s fallback + # so add_mutations stays aligned with new_ids + prefix = op.item_id.split("-", 1)[0] if "-" in op.item_id else "" + section_name = _PREFIX_TO_SECTION.get(prefix) + if section_name: + mut = Mutation( + step=self._distill_step, + type=OpType.ADD, + item_id="", # back-filled from new_ids + section=section_name, + content=op.content, + previous_content=None, + topic_ids=list(self._topic_ids), + ) + mutations.append(mut) + add_mutations.append(mut) + # else: topic-ID — apply() already skipped, nothing to record elif op.type == OpType.ADD and op.section and op.content: mut = Mutation( step=self._distill_step, diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index 4f7b7bf..023ed37 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -56,7 +56,9 @@ class CartographerSig(dspy.Signature): Each operation has exactly these fields: - type: one of "ADD", "DELETE", or "REPLACE" - section: (ADD only) one of the six section names - - item_id: (DELETE/REPLACE only) existing item ID from current memory + - item_id: (DELETE/REPLACE only) existing item ID from current memory. + Item IDs have short prefixes (cu-, cr-, dc-, ps-, rr-, ac-). + NEVER use topic IDs (owner/repo paths or URLs) as item_id. - content: (ADD/REPLACE only) the new content string Only reference `item_id`s that exist in the current memory. Never invent @@ -108,12 +110,15 @@ class CartographerSig(dspy.Signature): """ diagnosis: str = dspy.InputField(desc="Distiller's narrative diagnosis.") - item_tags: dict[str, ItemTag] = dspy.InputField(desc="Per-item tags from the Distiller.") + item_tags: dict[str, ItemTag] = dspy.InputField( + desc="Per-item tags from the Distiller. Keys are item IDs (prefixed cu-, cr-, dc-, ps-, rr-, ac-), never topic IDs." + ) cache_candidates: list[CacheCandidate] = dspy.InputField( desc="Candidate items the Distiller proposed." ) current_map: ContextMemory = dspy.InputField( - desc="Current context memory." + desc="Current context memory. 'topics' are metadata (not editable items). " + "Editable items live in the six sections and have prefixed IDs (cu-, cr-, dc-, ps-, rr-, ac-)." ) question: str = dspy.InputField(desc="Question the agent was answering.") token_budget: int = dspy.InputField(desc="Hard token budget for the context memory.") diff --git a/src/codespy/agents/memory/pg0_manager.py b/src/codespy/agents/memory/pg0_manager.py new file mode 100644 index 0000000..d983e6f --- /dev/null +++ b/src/codespy/agents/memory/pg0_manager.py @@ -0,0 +1,275 @@ +"""pg0-embedded lifecycle management for local PostgreSQL development.""" + +from __future__ import annotations + +import atexit +import logging +import time +from pathlib import Path +from typing import TYPE_CHECKING + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from pg0 import Pg0 + +_pg0_instance: Pg0 | None = None +_pg0_uri: str | None = None + + +def _wait_for_ready( + instance, + timeout: float = 5.0, +) -> str | None: + """Poll info() until pg0 reports a healthy instance with a URI. + + The pg0 binary's ``info`` command runs ``psql -c 'SELECT 1'`` + internally and only returns a URI when the healthcheck passes. + Polling this accounts for slow first-run initialization (initdb, + pgvector installation) without reimplementing the healthcheck. + + Returns: + Connection URI string, or None on timeout. + """ + deadline = time.monotonic() + timeout + last_info = None + last_exc = None + polls = 0 + while time.monotonic() < deadline: + polls += 1 + try: + last_info = instance.info() + last_exc = None + if last_info and last_info.uri: + return last_info.uri + logger.debug( + "pg0 poll #%d: running=%s, pid=%s, port=%s, uri=%s", + polls, + getattr(last_info, "running", None), + getattr(last_info, "pid", None), + getattr(last_info, "port", None), + bool(getattr(last_info, "uri", None)), + ) + except Exception as exc: + last_exc = exc + logger.debug("pg0 poll #%d: info() raised %s: %s", polls, type(exc).__name__, exc) + time.sleep(0.5) + + # Timeout — log summary + if last_exc is not None: + logger.debug( + "pg0 readiness timed out after %.0fs (%d polls); " + "last info() error: %s: %s", + timeout, polls, type(last_exc).__name__, last_exc, + ) + elif last_info is not None: + logger.debug( + "pg0 readiness timed out after %.0fs (%d polls); " + "last state: running=%s, pid=%s, port=%s, data_dir=%s", + timeout, polls, + getattr(last_info, "running", None), + getattr(last_info, "pid", None), + getattr(last_info, "port", None), + getattr(last_info, "data_dir", None), + ) + else: + logger.debug( + "pg0 readiness timed out after %.0fs (%d polls); " + "info() never returned a result", + timeout, polls, + ) + return None + + +def _get_pg0_logs(instance, lines: int = 30) -> str: + """Try to capture PostgreSQL log output from the pg0 instance.""" + try: + log_output = instance.logs(lines=lines) + if log_output and log_output.strip(): + return log_output.strip() + except Exception as exc: + logger.debug("Could not retrieve pg0 logs: %s", exc) + return "(no pg0 logs available)" + + +def _try_direct_healthcheck(instance, name: str) -> str | None: + """Verify PostgreSQL via psycopg when pg0's bundled-psql health check fails. + + Constructs a URI from the Pg0 instance's known parameters and the port + reported by info(), then runs SELECT 1 through psycopg — the same driver + EpisodeStore will use. + + Returns the URI on success, None on failure. + """ + try: + info = instance.info() + port = getattr(info, "port", None) + if not port: + return None + + username = getattr(instance, "username", "postgres") + password = getattr(instance, "password", "postgres") + database = getattr(instance, "database", name) + uri = f"postgresql://{username}:{password}@127.0.0.1:{port}/{database}" + + import psycopg + with psycopg.connect(uri, connect_timeout=5) as conn: + conn.execute("SELECT 1") + + logger.info( + "pg0 psycopg healthcheck passed on port %d (pg0 bundled-psql unavailable)", + port, + ) + return uri + except Exception as exc: + logger.debug("Direct psycopg healthcheck failed: %s", exc) + return None + + +def _start_round( + instance, name: str, timeout: float, Pg0AlreadyRunningError: type +) -> str | None: + """Start pg0 instance and wait for a healthy URI. + + Tries pg0 native health check first, then psycopg fallback. + Returns URI on success, None on failure. + """ + try: + start_info = instance.start() + logger.debug( + "pg0 start: running=%s, pid=%s, port=%s, uri=%s", + getattr(start_info, "running", None), + getattr(start_info, "pid", None), + getattr(start_info, "port", None), + bool(getattr(start_info, "uri", None)), + ) + if start_info and start_info.uri: + return start_info.uri + except Pg0AlreadyRunningError: + logger.info("pg0 instance already running, reusing it") + + uri = _wait_for_ready(instance, timeout=timeout) + if not uri: + uri = _try_direct_healthcheck(instance, name) + return uri + + +def get_pg0_uri(name: str = "codespy", port: int | None = None, data_dir: str | None = None) -> str: + """Start pg0 (if not running) and return its connection URI. + + Strategy: + Round 1 — start + poll info() for readiness (15s timeout). + Round 2 — stop + restart without data loss (15s timeout). + If both fail, raise RuntimeError with manual recovery instructions. + Data is never dropped automatically. + + The pg0 binary's ``info`` command includes a ``SELECT 1`` healthcheck; + a URI is only returned when the database is genuinely healthy. + + Args: + name: Database/instance name to use. + port: Port to use (auto-detected if None). + + Returns: + PostgreSQL connection URI string. + + Raises: + ImportError: If pg0-embedded is not installed. + RuntimeError: If both rounds fail to produce a healthy instance + (data is preserved; see error message for manual recovery). + """ + global _pg0_instance, _pg0_uri + + if _pg0_instance is not None and _pg0_uri is not None: + return _pg0_uri + + from pg0 import Pg0, Pg0AlreadyRunningError # raises ImportError + + # Default data_dir to ~/.cache/codespy/pg0 so data persists on the + # Docker-mounted codespy-cache volume (and locally in the home dir). + # NOTE: cannot reuse ~/.cache/codespy/memory — old JSON episodes live there. + if data_dir is None: + data_dir = str(Path.home() / ".cache" / "codespy" / "pg0") + + logger.info("Starting pg0-embedded PostgreSQL%s...", f" on port {port}" if port else "") + _pg0_instance = Pg0(port=port, name=name, database=name, data_dir=data_dir) + + try: + # Round 1 + uri = _start_round(_pg0_instance, name, 15.0, Pg0AlreadyRunningError) + if uri: + atexit.register(stop_pg0) + _pg0_uri = uri + logger.info("pg0-embedded PostgreSQL ready at %s", uri) + try: + _ready_info = _pg0_instance.info() + _data_dir = getattr(_ready_info, "data_dir", None) + if _data_dir: + logger.info("pg0 data_dir: %s (exists=%s)", _data_dir, Path(_data_dir).is_dir()) + except Exception: + pass # non-critical diagnostic + return uri + + # Round 2: stop + restart (data preserved) + logger.warning( + "pg0 not healthy after Round 1; stopping and retrying (data preserved)..." + ) + try: + _pg0_instance.stop() + except Exception: + pass + time.sleep(2) + + _pg0_instance = Pg0(port=port, name=name, database=name, data_dir=data_dir) + uri = _start_round(_pg0_instance, name, 15.0, Pg0AlreadyRunningError) + if uri: + atexit.register(stop_pg0) + _pg0_uri = uri + logger.info("pg0-embedded PostgreSQL ready (after restart) at %s", uri) + try: + _ready_info = _pg0_instance.info() + _data_dir = getattr(_ready_info, "data_dir", None) + if _data_dir: + logger.info("pg0 data_dir: %s (exists=%s)", _data_dir, Path(_data_dir).is_dir()) + except Exception: + pass # non-critical diagnostic + return uri + + # Both failed + pg_logs = _get_pg0_logs(_pg0_instance) + raise RuntimeError( + f"pg0-embedded failed to start PostgreSQL instance '{name}' " + f"after two attempts (data preserved).\n" + f"PostgreSQL logs:\n{pg_logs}\n\n" + f"Troubleshooting:\n" + f" 1. Check if another PostgreSQL is using the same port\n" + f" 2. Check pg0 status: pg0 info --name {name}\n" + f" 3. View logs: pg0 logs --name {name}\n" + f" 4. If data is corrupt, manually reset: " + f"pg0 drop --name {name} && rm -rf ~/.pg0/instances/{name}" + ) + + except Exception: + if _pg0_instance is not None: + try: + _pg0_instance.stop() + except Exception: + pass + _pg0_instance = None + _pg0_uri = None + raise + + +def stop_pg0() -> None: + """Stop the cached pg0 instance (if running).""" + global _pg0_instance, _pg0_uri + + if _pg0_instance is not None: + logger.info("Stopping pg0-embedded PostgreSQL...") + try: + _pg0_instance.stop() + except Exception as e: + logger.warning(f"Error stopping pg0: {e}") + finally: + _pg0_instance = None + _pg0_uri = None diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py new file mode 100644 index 0000000..a410352 --- /dev/null +++ b/src/codespy/agents/memory/postgres.py @@ -0,0 +1,746 @@ +"""PostgreSQL-backed episode persistence for Hippocampus memory.""" + +from __future__ import annotations + +import logging +import uuid +from datetime import UTC, datetime +from typing import TYPE_CHECKING + +from psycopg import sql +from psycopg.rows import dict_row +from psycopg_pool import ConnectionPool + +if TYPE_CHECKING: + from codespy.agents.memory.hippocampus.context_memory import ( + ContextMemory, + Item, + Mutation, + Topic, + ) + from codespy.agents.memory.hippocampus.episode import Episode + +logger = logging.getLogger(__name__) + + +class EpisodeStore: + """PostgreSQL-backed episode persistence for Hippocampus memory. + + Uses psycopg ConnectionPool for thread-safe background saves. + Tables are auto-created on first connect if they don't exist. + """ + + def __init__( + self, + conninfo: str, + bank_id: str, + min_size: int = 1, + max_size: int = 4, + ): + """Create store with a psycopg ConnectionPool, scoped to a bank. + + Args: + conninfo: PostgreSQL connection string (e.g., postgresql://localhost:5432/dbname) + bank_id: Identifier for the bank (nickname, username, email, agent name) + min_size: Minimum connections in pool + max_size: Maximum connections in pool + """ + self.bank_id = bank_id + self._pool = ConnectionPool( + conninfo=conninfo, + min_size=min_size, + max_size=max_size, + ) + self._pool.open() + self.ensure_schema() + + def close(self) -> None: + """Shut down the connection pool.""" + self._pool.close() + + def ensure_schema(self) -> None: + """Auto-create tables if not present (idempotent).""" + with self._pool.connection() as conn: + with conn.cursor() as cur: + # Extensions + cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + cur.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm") + + # Schema version table + cur.execute(""" + CREATE TABLE IF NOT EXISTS schema_version ( + version INT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + """) + + # Banks table + cur.execute(""" + CREATE TABLE IF NOT EXISTS banks ( + id VARCHAR(64) PRIMARY KEY, + description TEXT + ) + """) + + # Topics table + cur.execute(""" + CREATE TABLE IF NOT EXISTS topics ( + bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, + id VARCHAR(512) NOT NULL, + description TEXT NOT NULL, + description_embedding vector(1536), + description_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('english', coalesce(description, '')) + ) STORED, + PRIMARY KEY (bank_id, id) + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_topics_desc_embedding + ON topics USING hnsw (description_embedding vector_cosine_ops) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_topics_desc_tsv + ON topics USING gin (description_tsv) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_topics_id_trgm + ON topics USING gin (id gin_trgm_ops) + """) + + # Episodes table + cur.execute(""" + CREATE TABLE IF NOT EXISTS episodes ( + bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, + id UUID NOT NULL, + run_id VARCHAR(48) NOT NULL, + task VARCHAR(64) NOT NULL, + module VARCHAR(64) NOT NULL, + question TEXT NOT NULL DEFAULT '', + timestamp TIMESTAMPTZ NOT NULL, + question_embedding vector(1536), + question_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('english', coalesce(question, '')) + ) STORED, + PRIMARY KEY (bank_id, id) + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_episodes_task_time + ON episodes (bank_id, task, timestamp DESC) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_episodes_question_embedding + ON episodes USING hnsw (question_embedding vector_cosine_ops) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_episodes_question_tsv + ON episodes USING gin (question_tsv) + """) + + # Episode topics junction + cur.execute(""" + CREATE TABLE IF NOT EXISTS episode_topics ( + bank_id VARCHAR(64) NOT NULL, + episode_id UUID NOT NULL, + topic_id VARCHAR(512) NOT NULL, + PRIMARY KEY (bank_id, episode_id, topic_id), + FOREIGN KEY (bank_id, episode_id) + REFERENCES episodes(bank_id, id) ON DELETE CASCADE, + FOREIGN KEY (bank_id, topic_id) + REFERENCES topics(bank_id, id) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_episode_topics_reverse + ON episode_topics (bank_id, topic_id) + """) + + # Items table with flattened mutation fields + cur.execute(""" + CREATE TABLE IF NOT EXISTS items ( + bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, + id VARCHAR(48) NOT NULL, + version INT NOT NULL DEFAULT 1, + section VARCHAR(32) NOT NULL, + content TEXT, + episode_id UUID NOT NULL, + step INT NOT NULL DEFAULT 0, + op_type VARCHAR(8) NOT NULL, + previous_content TEXT, + ordinal INT NOT NULL DEFAULT 0, + content_embedding vector(1536), + content_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('english', coalesce(content, '')) + ) STORED, + PRIMARY KEY (bank_id, id, version), + FOREIGN KEY (bank_id, episode_id) + REFERENCES episodes(bank_id, id) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_items_episode + ON items (bank_id, episode_id) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_items_content_embedding + ON items USING hnsw (content_embedding vector_cosine_ops) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_items_content_tsv + ON items USING gin (content_tsv) + """) + + # Item topics junction + cur.execute(""" + CREATE TABLE IF NOT EXISTS item_topics ( + bank_id VARCHAR(64) NOT NULL, + item_id VARCHAR(48) NOT NULL, + item_version INT NOT NULL, + topic_id VARCHAR(512) NOT NULL, + item_occurrence INT NOT NULL DEFAULT 0, + version_occurrence INT NOT NULL DEFAULT 0, + PRIMARY KEY (bank_id, item_id, item_version, topic_id), + FOREIGN KEY (bank_id, item_id, item_version) + REFERENCES items(bank_id, id, version) ON DELETE CASCADE, + FOREIGN KEY (bank_id, topic_id) + REFERENCES topics(bank_id, id) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_item_topics_reverse + ON item_topics (bank_id, topic_id) + """) + + # Episode items junction + cur.execute(""" + CREATE TABLE IF NOT EXISTS episode_items ( + bank_id VARCHAR(64) NOT NULL, + episode_id UUID NOT NULL, + item_id VARCHAR(48) NOT NULL, + item_version INT NOT NULL, + PRIMARY KEY (bank_id, episode_id, item_id), + FOREIGN KEY (bank_id, episode_id) + REFERENCES episodes(bank_id, id) ON DELETE CASCADE, + FOREIGN KEY (bank_id, item_id, item_version) + REFERENCES items(bank_id, id, version) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_episode_items_reverse + ON episode_items (bank_id, item_id) + """) + + # Artifacts table + cur.execute(""" + CREATE TABLE IF NOT EXISTS artifacts ( + bank_id VARCHAR(64) NOT NULL, + episode_id UUID NOT NULL, + name TEXT NOT NULL, + content TEXT NOT NULL, + content_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('english', coalesce(content, '')) + ) STORED, + PRIMARY KEY (bank_id, episode_id, name), + FOREIGN KEY (bank_id, episode_id) + REFERENCES episodes(bank_id, id) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_artifacts_content_tsv + ON artifacts USING gin (content_tsv) + """) + + conn.commit() + + def verify_access(self) -> None: + """Check connection health. Raises on failure.""" + with self._pool.connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT 1") + + def save_episode(self, episode: Episode) -> None: + """Persist episode. Idempotent on episode data. + + Single transaction: + 1. Ensure bank row exists (idempotent) + 2. Upsert episode row (no-op if already exists) + 3. Upsert topics, insert items (versioned), insert junctions, insert artifacts + """ + # Import here to avoid circular imports + from codespy.agents.memory.hippocampus.context_memory import ( + ContextMemory, + Item, + Mutation, + OpType, + Topic, + ) + + with self._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + # 1. Ensure bank exists + cur.execute( + "INSERT INTO banks (id) VALUES (%s) ON CONFLICT (id) DO NOTHING", + (self.bank_id,), + ) + + # 2. Upsert episode + cur.execute( + """ + INSERT INTO episodes (bank_id, id, run_id, task, module, question, timestamp) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (bank_id, id) DO NOTHING + """, + ( + self.bank_id, + str(episode.id), + episode.run_id, + episode.task, + episode.module, + episode.question, + episode.timestamp, + ), + ) + + # 3. Upsert topics + topic_ids: set[str] = set() + for topic in episode.context_memory.topics: + topic_ids.add(topic.id) + cur.execute( + """ + INSERT INTO topics (bank_id, id, description) + VALUES (%s, %s, %s) + ON CONFLICT (bank_id, id) DO UPDATE SET description = EXCLUDED.description + """, + (self.bank_id, topic.id, topic.description), + ) + + # 4. Insert episode_topics junctions + for topic in episode.context_memory.topics: + cur.execute( + """ + INSERT INTO episode_topics (bank_id, episode_id, topic_id) + VALUES (%s, %s, %s) + ON CONFLICT (bank_id, episode_id, topic_id) DO NOTHING + """, + (self.bank_id, str(episode.id), topic.id), + ) + + # 5. Process items and mutations + # Build map of item_id -> mutation for quick lookup + mutation_map: dict[str, Mutation] = {} + for mut in episode.mutations: + mutation_map[mut.item_id] = mut + + # Get all current items from context_memory + current_items: dict[str, tuple[str, Item]] = {} # item_id -> (section, item) + for sec in episode.context_memory.section_names(): + for item in getattr(episode.context_memory, sec): + current_items[item.id] = (sec, item) + + # Track items we've processed to detect DELETEs + processed_item_ids: set[str] = set() + + # Process items in current context memory + for item_id, (section, item) in current_items.items(): + processed_item_ids.add(item_id) + mutation = mutation_map.get(item_id) + + # Determine if this is a new item (ADD) or existing (REPLACE/inherited) + is_new = False + version = 1 + op_type = "ADD" + previous_content: str | None = None + + if mutation: + is_new = mutation.type == OpType.ADD + if mutation.type == OpType.REPLACE: + op_type = "REPLACE" + previous_content = mutation.previous_content + elif mutation.type == OpType.ADD: + op_type = "ADD" + else: + # Inherited item - need to look up existing version + cur.execute( + """ + SELECT MAX(version) as max_ver + FROM items + WHERE bank_id = %s AND id = %s + """, + (self.bank_id, item_id), + ) + row = cur.fetchone() + if row and row["max_ver"]: + version = row["max_ver"] + op_type = "REPLACE" # Already exists + + # Insert new item version only if it's ADD or REPLACE + if mutation and mutation.type in (OpType.ADD, OpType.REPLACE): + # Get next version number + cur.execute( + """ + SELECT COALESCE(MAX(version), 0) + 1 as next_ver + FROM items + WHERE bank_id = %s AND id = %s + """, + (self.bank_id, item_id), + ) + row = cur.fetchone() + version = row["next_ver"] if row else 1 + + # Find step from mutation + step = mutation.step if mutation else 0 + + cur.execute( + """ + INSERT INTO items + (bank_id, id, version, section, content, episode_id, step, op_type, previous_content, ordinal) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.bank_id, + item_id, + version, + section, + item.content, + str(episode.id), + step, + op_type, + previous_content, + 0, + ), + ) + + # Insert episode_items junction + cur.execute( + """ + INSERT INTO episode_items (bank_id, episode_id, item_id, item_version) + VALUES (%s, %s, %s, %s) + ON CONFLICT (bank_id, episode_id, item_id) DO NOTHING + """, + (self.bank_id, str(episode.id), item_id, version), + ) + + # Insert item_topics for this version + for topic_id in item.topic_ids: + # Count occurrences + cur.execute( + """ + SELECT item_occurrence, version_occurrence + FROM item_topics + WHERE bank_id = %s AND item_id = %s AND topic_id = %s + ORDER BY item_version DESC + LIMIT 1 + """, + (self.bank_id, item_id, topic_id), + ) + prev_row = cur.fetchone() + if prev_row: + item_occ = prev_row["item_occurrence"] + 1 + ver_occ = ( + prev_row["version_occurrence"] + 1 + if is_new + else 1 + ) + else: + item_occ = 1 + ver_occ = 1 + + cur.execute( + """ + INSERT INTO item_topics + (bank_id, item_id, item_version, topic_id, item_occurrence, version_occurrence) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (bank_id, item_id, item_version, topic_id) DO NOTHING + """, + ( + self.bank_id, + item_id, + version, + topic_id, + item_occ, + ver_occ, + ), + ) + else: + # Inherited item - use existing version + cur.execute( + """ + SELECT MAX(version) as max_ver + FROM items + WHERE bank_id = %s AND id = %s + """, + (self.bank_id, item_id), + ) + row = cur.fetchone() + existing_version = row["max_ver"] if row and row["max_ver"] else 1 + + # Insert episode_items junction with existing version + cur.execute( + """ + INSERT INTO episode_items (bank_id, episode_id, item_id, item_version) + VALUES (%s, %s, %s, %s) + ON CONFLICT (bank_id, episode_id, item_id) DO NOTHING + """, + ( + self.bank_id, + str(episode.id), + item_id, + existing_version, + ), + ) + + # Increment item_occurrence for inherited items + for topic_id in item.topic_ids: + cur.execute( + """ + SELECT item_occurrence + FROM item_topics + WHERE bank_id = %s AND item_id = %s AND topic_id = %s + AND item_version = %s + """, + (self.bank_id, item_id, topic_id, existing_version), + ) + occ_row = cur.fetchone() + if occ_row: + new_occ = occ_row["item_occurrence"] + 1 + cur.execute( + """ + UPDATE item_topics + SET item_occurrence = %s + WHERE bank_id = %s AND item_id = %s AND topic_id = %s + AND item_version = %s + """, + ( + new_occ, + self.bank_id, + item_id, + topic_id, + existing_version, + ), + ) + + # 6. Process DELETE mutations (items not in current context) + for mutation in episode.mutations: + if mutation.type == OpType.DELETE and mutation.item_id: + if mutation.item_id not in processed_item_ids: + # Get next version number + cur.execute( + """ + SELECT COALESCE(MAX(version), 0) + 1 as next_ver + FROM items + WHERE bank_id = %s AND id = %s + """, + (self.bank_id, mutation.item_id), + ) + row = cur.fetchone() + version = row["next_ver"] if row else 1 + + cur.execute( + """ + INSERT INTO items + (bank_id, id, version, section, content, episode_id, step, op_type, previous_content, ordinal) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.bank_id, + mutation.item_id, + version, + mutation.section, + None, # content is NULL for DELETE + str(episode.id), + mutation.step, + "DELETE", + mutation.previous_content, + 0, + ), + ) + # Note: DELETE items are NOT inserted into episode_items + + # 7. Insert artifacts + for name, content in (episode.artifacts or {}).items(): + cur.execute( + """ + INSERT INTO artifacts (bank_id, episode_id, name, content) + VALUES (%s, %s, %s, %s) + ON CONFLICT (bank_id, episode_id, name) DO UPDATE SET content = EXCLUDED.content + """, + (self.bank_id, str(episode.id), name, content), + ) + + conn.commit() + + logger.info( + "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, items=%d)", + episode.id, self.bank_id, episode.task, + len(episode.context_memory.topics), + len(episode.context_memory.all_items()), + ) + + def load_context( + self, + task: str, + topic_ids: list[str] | None = None, + topic_prefix: str | None = None, + ) -> ContextMemory | None: + """Load context for a new episode: latest item versions from the + most recent episode for this (bank, task, topics). + + Args: + task: Task name to filter by. + topic_ids: Exact topic IDs to match (ANY). + topic_prefix: Prefix match on topic ID (e.g. repo_full_name + matches 'owner/repo', 'owner/repo/pkg', etc.). + Mutually exclusive with topic_ids. + + Returns ContextMemory (topics + items + bindings) or None if + no prior episode exists. No mutations, artifacts, or episode + metadata are loaded. + """ + try: + # Import here to avoid circular imports + from codespy.agents.memory.hippocampus.context_memory import ( + ContextMemory, + Item, + Topic, + ) + + with self._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + # 1. Find the latest episode for this task + topics + topic_filter = "" + params: list = [self.bank_id, task] + + if topic_ids: + topic_filter = "AND et.topic_id = ANY(%s)" + params.append(topic_ids) + elif topic_prefix: + topic_filter = "AND et.topic_id LIKE %s" + params.append(f"{topic_prefix}%") + + cur.execute( + f""" + SELECT e.id FROM episodes e + JOIN episode_topics et ON et.bank_id = e.bank_id AND et.episode_id = e.id + WHERE e.bank_id = %s AND e.task = %s {topic_filter} + ORDER BY e.timestamp DESC + LIMIT 1 + """, + params, + ) + row = cur.fetchone() + if not row: + # Diagnostic: how many episodes exist for this bank+task (ignoring topic filter)? + cur.execute( + "SELECT COUNT(*) AS cnt FROM episodes WHERE bank_id = %s AND task = %s", + (self.bank_id, task), + ) + cnt = cur.fetchone()["cnt"] + logger.info( + "load_context: no episode matched (bank=%s, task=%s, topics=%s, prefix=%s); " + "total episodes for bank+task: %d", + self.bank_id, task, topic_ids, topic_prefix, cnt, + ) + return None + + episode_id = row["id"] + logger.debug("load_context: found episode %s (bank=%s, task=%s)", episode_id, self.bank_id, task) + + # 2. Load topics from that episode + cur.execute( + """ + SELECT t.id, t.description FROM episode_topics et + JOIN topics t ON t.bank_id = et.bank_id AND t.id = et.topic_id + WHERE et.bank_id = %s AND et.episode_id = %s + """, + (self.bank_id, episode_id), + ) + topics = [ + Topic(id=row["id"], description=row["description"]) + for row in cur.fetchall() + ] + + # 3. Load items at their LATEST version (not the pinned version) + cur.execute( + """ + SELECT DISTINCT ON (i.id) i.id, i.section, i.content, i.version + FROM episode_items ei + JOIN items i ON i.bank_id = ei.bank_id AND i.id = ei.item_id + WHERE ei.bank_id = %s AND ei.episode_id = %s + AND i.op_type != 'DELETE' + ORDER BY i.id, i.version DESC + """, + (self.bank_id, episode_id), + ) + items_by_id: dict[str, dict] = {} + for row in cur.fetchall(): + items_by_id[row["id"]] = { + "section": row["section"], + "content": row["content"], + "version": row["version"], + } + + # 4. Load item-topic bindings for those latest versions + if items_by_id: + cur.execute( + """ + SELECT it.item_id, it.topic_id, it.item_occurrence, it.version_occurrence + FROM item_topics it + WHERE it.bank_id = %s + AND (it.item_id, it.item_version) IN ( + SELECT i.id, MAX(i.version) + FROM episode_items ei + JOIN items i ON i.bank_id = ei.bank_id AND i.id = ei.item_id + WHERE ei.bank_id = %s AND ei.episode_id = %s AND i.op_type != 'DELETE' + GROUP BY i.id + ) + """, + (self.bank_id, self.bank_id, episode_id), + ) + item_topics: dict[str, list[str]] = {} + for row in cur.fetchall(): + item_id = row["item_id"] + if item_id not in item_topics: + item_topics[item_id] = [] + item_topics[item_id].append(row["topic_id"]) + + # Build ContextMemory sections + sections: dict[str, list[Item]] = { + "context_roadmap": [], + "context_understanding": [], + "domain_constants": [], + "actions": [], + "parsing_schema": [], + "reusable_results": [], + } + + for item_id, item_data in items_by_id.items(): + section = item_data["section"] + if section not in sections: + section = "context_understanding" # fallback + + item = Item( + id=item_id, + content=item_data["content"], + topic_ids=item_topics.get(item_id, []), + ) + sections[section].append(item) + + # Build ContextMemory + ctx = ContextMemory(topics=topics) + for section_name, items in sections.items(): + setattr(ctx, section_name, items) + + total_items = sum(len(items) for items in sections.values()) + logger.debug("load_context: loaded %d topics, %d items from episode %s", len(topics), total_items, episode_id) + + return ctx + + # No items - return empty ContextMemory with topics + logger.debug("load_context: loaded %d topics, 0 items from episode %s", len(topics), episode_id) + return ContextMemory(topics=topics) + except Exception: + logger.warning("load_context failed (bank=%s, task=%s)", self.bank_id, task, exc_info=True) + return None diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 1529883..69b1018 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -33,6 +33,15 @@ class PRContext(BaseModel): pr_description: str = Field(default="", description="PR body/description") summary: str = Field(description="2-3 sentence PR summary produced by Summarizer") + @property + def repo_full_name(self) -> str: + """Get owner/repo from host-qualified repo_slug.""" + # repo_slug is "github.com/owner/repo" or just "owner/repo" + parts = self.repo_slug.split("/") + if len(parts) >= 3: + return "/".join(parts[1:]) # strip host + return self.repo_slug + def to_topic(self) -> "Topic": """Build a Topic representing this PR. diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 90ef985..9089685 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -8,10 +8,11 @@ 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.context_memory import Topic 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.config_memory import get_episode_store if TYPE_CHECKING: from codespy.agents.reviewer.models import ScopeResult @@ -66,19 +67,36 @@ def _call_auditor( # Load prior "audit" episodes per scope initial_memory: ContextMemory | None = None + topic_ids: list[str] | None = None + store = None if self._settings.get_memory_enabled("audit") 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="audit", exclude_run_id=run_id) - if ep is not None: - per_scope_memories.append(ep.context_memory) - if per_scope_memories: - initial_memory = ContextMemory.merge(*per_scope_memories) - logger.info("Merged %d prior audit episode(s) into auditor memory", len(per_scope_memories)) - - if self._settings.get_memory_enabled("audit"): + store = get_episode_store(self._settings) + if store is not None: + # Build topic_ids from scope topics + topic_ids = [] + for scope in scopes: + if scope.topic(review_context.pr_context.repo_full_name): + topic_ids.append(scope.topic(review_context.pr_context.repo_full_name).id) + initial_memory = store.load_context( + task="audit", + topic_ids=topic_ids if topic_ids else None, + ) + if initial_memory: + logger.info("Loaded prior audit episode(s) into auditor memory") + else: + logger.info("No prior audit episode found") + + if self._settings.get_memory_enabled("audit") and store is not None: + # Build topics list for Hippocampus + scope_topics: list[Topic] = [] + for scope in scopes or []: + scope_topic = scope.topic(review_context.pr_context.repo_full_name) + if scope_topic: + scope_topics.append(Topic( + id=scope_topic.id, + description=scope_topic.description, + )) + mem = Hippocampus( auditor, budget=self._settings.get_memory_budget("audit"), @@ -87,7 +105,7 @@ def _call_auditor( task_name="audit", run_id=run_id, initial_memory=initial_memory, - topics=topics, + topics=scope_topics if scope_topics else None, ) result = mem( pr_title=review_context.pr_context.pr_title, @@ -96,21 +114,13 @@ def _call_auditor( ) # Run episode save synchronously (auditor is the last module) try: - _store = get_memory_store(self._settings) - _common_dir = ( - _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" f"## Recommendation\n\n{result.recommendation}\n" ) } - mem.end_episode(_store, _common_dir, artifacts=_artifacts) - if scopes: - for scope in scopes: - mem.save_episode(_store, mem.episode_file_path(scope.scope_path())) + mem.end_episode(store, artifacts=_artifacts) except Exception: logger.warning("Audit episode save failed", exc_info=True) else: diff --git a/src/codespy/agents/reviewer/modules/code_reviewer.py b/src/codespy/agents/reviewer/modules/code_reviewer.py index 651ebef..0c95cb2 100644 --- a/src/codespy/agents/reviewer/modules/code_reviewer.py +++ b/src/codespy/agents/reviewer/modules/code_reviewer.py @@ -12,7 +12,7 @@ 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.episode import find_latest_episode, submit_episode_save +from codespy.agents.memory.hippocampus.episode import submit_episode_save from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( issues_to_markdown, @@ -21,7 +21,7 @@ restore_repo_paths, ) from codespy.config import get_settings -from codespy.config_memory import get_memory_store +from codespy.config_memory import get_episode_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -292,16 +292,20 @@ async def _review_scope( async with SignatureContext("code_review", self._cost_tracker): # Load own prior "code_review" episode for this scope scope_initial_memory: ContextMemory | None = None + store = None if self._settings.get_memory_enabled("code_review"): - ep = find_latest_episode( - get_memory_store(self._settings), - scope.scope_path(), - task="code_review", - exclude_run_id=run_id, - ) - if ep is not None: - scope_initial_memory = ep.context_memory - if self._settings.get_memory_enabled("code_review"): + store = get_episode_store(self._settings) + if store is not None: + topic_ids = [scope.topic(review_context.pr_context.repo_full_name).id] + scope_initial_memory = store.load_context( + task="code_review", + topic_ids=topic_ids, + ) + if scope_initial_memory: + logger.info("Loaded prior code_review episode for scope %s", scope.subroot) + else: + logger.info("No prior code_review episode for scope %s", scope.subroot) + if self._settings.get_memory_enabled("code_review") and store is not None: question = ( f"review code change of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -330,12 +334,10 @@ async def _review_scope( if issue.confidence >= self._settings.min_confidence ] # Fire-and-forget background episode save - _store = get_memory_store(self._settings) - _scope_path = scope.scope_path() _artifacts = {"review": issues_to_markdown(issues)} - def _persist(m=mem, s=_store, p=_scope_path, a=_artifacts): + def _persist(m=mem, s=store, a=_artifacts): try: - m.end_episode(s, p, artifacts=a) + m.end_episode(s, artifacts=a) except Exception: logger.warning("Background code_review episode save failed", exc_info=True) submit_episode_save(_persist, name="code-review-episode-save") diff --git a/src/codespy/agents/reviewer/modules/doc_reviewer.py b/src/codespy/agents/reviewer/modules/doc_reviewer.py index 2095090..a77cd93 100644 --- a/src/codespy/agents/reviewer/modules/doc_reviewer.py +++ b/src/codespy/agents/reviewer/modules/doc_reviewer.py @@ -12,7 +12,7 @@ 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.episode import find_latest_episode, submit_episode_save +from codespy.agents.memory.hippocampus.episode import submit_episode_save 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 ( @@ -23,7 +23,7 @@ restore_repo_paths, ) from codespy.config import get_settings -from codespy.config_memory import get_memory_store +from codespy.config_memory import get_episode_store logger = logging.getLogger(__name__) @@ -214,16 +214,20 @@ async def _review_scope( async with SignatureContext("doc", self._cost_tracker): # Load own prior "doc" episode for this scope scope_initial_memory: ContextMemory | None = None + store = None if self._settings.get_memory_enabled("doc"): - ep = find_latest_episode( - get_memory_store(self._settings), - scope.scope_path(), - task="doc", - exclude_run_id=run_id, - ) - if ep is not None: - scope_initial_memory = ep.context_memory - if self._settings.get_memory_enabled("doc"): + store = get_episode_store(self._settings) + if store is not None: + topic_ids = [scope.topic(review_context.pr_context.repo_full_name).id] + scope_initial_memory = store.load_context( + task="doc", + topic_ids=topic_ids, + ) + if scope_initial_memory: + logger.info("Loaded prior doc episode for scope %s", scope.subroot) + else: + logger.info("No prior doc episode for scope %s", scope.subroot) + if self._settings.get_memory_enabled("doc") and store is not None: question = ( f"review documentation of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -253,12 +257,10 @@ async def _review_scope( if issue.confidence >= self._settings.min_confidence ] # Fire-and-forget background episode save - _store = get_memory_store(self._settings) - _scope_path = scope.scope_path() _artifacts = {"review": issues_to_markdown(issues)} - def _persist(m=mem, s=_store, p=_scope_path, a=_artifacts): + def _persist(m=mem, s=store, a=_artifacts): try: - m.end_episode(s, p, artifacts=a) + m.end_episode(s, artifacts=a) except Exception: logger.warning("Background doc episode save failed", exc_info=True) submit_episode_save(_persist, name="doc-episode-save") diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index d49a60a..feabb76 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -11,7 +11,6 @@ import fnmatch import logging import os -from codespy.agents.memory.hippocampus.episode import submit_episode_save from pathlib import Path from typing import Any @@ -21,6 +20,7 @@ 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.episode import submit_episode_save from codespy.agents.reviewer.models import ( PackageManifest, ReviewContext, @@ -29,7 +29,7 @@ ) from codespy.agents.reviewer.modules.manifest_parser import extract_package_name from codespy.config import get_settings -from codespy.config_memory import get_memory_store +from codespy.config_memory import get_episode_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 @@ -1066,12 +1066,20 @@ async def _refine_scopes( f"{review_context.pr_context.summary}" ) # Scope resolver loads its own prior episodes (no memory inheritance) - from codespy.agents.memory.hippocampus.episode import find_latest_episode - store = get_memory_store(self._settings) - # Use repo root as the scope path for scope resolver episodes - scope_path = f"/{review_context.pr_context.repo_slug}/" - ep = find_latest_episode(store, scope_path, task="scope", exclude_run_id=run_id) - scope_initial_memory: ContextMemory | None = ep.context_memory if ep is not None else None + store = get_episode_store(self._settings) + scope_initial_memory: ContextMemory | None = None + if store is not None: + # Load by repo prefix — matches any scope-level topic + # (e.g. 'khezen/codespy' matches 'khezen/codespy/codespy-ai') + repo_topic_id = pr.repo_full_name + scope_initial_memory = store.load_context( + task="scope", + topic_prefix=repo_topic_id, + ) + if scope_initial_memory: + logger.info("Loaded prior scope episode for %s", repo_topic_id) + else: + logger.info("No prior scope episode for %s", repo_topic_id) mem = Hippocampus( agent, budget=self._settings.get_memory_budget("scope"), @@ -1160,16 +1168,14 @@ async def _refine_scopes( mem.cmem.bind_topics(scope_topics, stamp_topic_ids) # Fire-and-forget background episode save - if mem is not None: - common_dir = _deepest_common_folder(final_scopes, pr.repo_slug) + if mem is not None and store is not None: scope_desc = "\n".join( f"- {s.subroot} ({s.scope_type.value}): {len(s.changed_files)} files" for s in final_scopes ) - store = get_memory_store(self._settings) def _persist(): try: - mem.end_episode(store, common_dir, artifacts={"scopes": scope_desc}) + mem.end_episode(store, artifacts={"scopes": scope_desc}) except Exception: logger.warning("Background scope episode save failed", exc_info=True) submit_episode_save(_persist, name="scope-episode-save") diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index f9d1a07..1b48b8e 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -1,7 +1,6 @@ """PR summarizer module — produces a concise summary before scope identification.""" import logging -from codespy.agents.memory.hippocampus.episode import submit_episode_save from typing import TYPE_CHECKING import dspy @@ -9,10 +8,11 @@ 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.episode import submit_episode_save from codespy.agents.memory.hippocampus.context_memory import Topic 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.config_memory import get_episode_store if TYPE_CHECKING: from codespy.agents.reviewer.models import PRContext, ScopeResult @@ -73,28 +73,27 @@ def forward( logger.debug("Skipping summary: disabled") return pr_context.pr_title or "No title" - # Load latest "summary" episode per scope and merge + # Load latest "summary" episode for the given topics initial_memory: ContextMemory | None = None + store = None + topic_ids: list[str] | None = None 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(), + store = get_episode_store(self._settings) + if store is not None: + # Build topic_ids from scope topics + topic_ids = [] + for scope in scopes: + if scope.topic(pr_context.repo_full_name): + topic_ids.append(scope.topic(pr_context.repo_full_name).id) + initial_memory = store.load_context( task="summary", - exclude_run_id=run_id, - ) - if ep is not None: - per_scope_memories.append(ep.context_memory) - if per_scope_memories: - initial_memory = ContextMemory.merge(*per_scope_memories) - logger.info( - "Merged %d prior summary episode(s) into summarizer memory", - len(per_scope_memories), + topic_ids=topic_ids if topic_ids else None, ) + if initial_memory: + logger.info("Loaded prior summary episode(s) into summarizer memory") + else: + logger.info("No prior summary episode found") + summarizer = ContextSafe( dspy.ChainOfThought(PRSummarySignature), PRSummarySignature, @@ -109,7 +108,17 @@ def forward( mem: Hippocampus | None = None with SignatureContext("summary", self._cost_tracker): - if self._settings.get_memory_enabled("summary"): + if self._settings.get_memory_enabled("summary") and store is not None: + # Build topics list for Hippocampus + scope_topics: list[Topic] = [] + for scope in scopes or []: + scope_topic = scope.topic(pr_context.repo_full_name) + if scope_topic: + scope_topics.append(Topic( + id=scope_topic.id, + description=scope_topic.description, + )) + mem = Hippocampus( summarizer, budget=self._settings.get_memory_budget("summary"), @@ -118,7 +127,7 @@ def forward( task_name="summary", run_id=run_id, initial_memory=initial_memory, - topics=topics, + topics=scope_topics if scope_topics else topics, ) result = mem( pr_title=pr_context.pr_title, @@ -127,16 +136,10 @@ def forward( patches=patches, ) # Fire-and-forget episode save - _store = get_memory_store(self._settings) - _common_dir = _deepest_common_folder(scopes, pr_context.repo_slug) if scopes else f"/{pr_context.repo_slug}/" _summary_text = result.summary - _scopes = scopes def _persist(): try: - mem.end_episode(_store, _common_dir, artifacts={"summary": _summary_text}) - if _scopes: - for scope in _scopes: - mem.save_episode(_store, mem.episode_file_path(scope.scope_path())) + mem.end_episode(store, artifacts={"summary": _summary_text}) except Exception: logger.warning("Background summary episode save failed", exc_info=True) submit_episode_save(_persist, name="summary-episode-save") diff --git a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py index ec84d3e..8a66e9a 100644 --- a/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +++ b/src/codespy/agents/reviewer/modules/supply_chain_auditor.py @@ -12,7 +12,7 @@ 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.episode import find_latest_episode, submit_episode_save +from codespy.agents.memory.hippocampus.episode import submit_episode_save from codespy.agents.reviewer.models import Issue, IssueCategory, ReviewContext, ScopeResult from codespy.agents.reviewer.modules.helpers import ( issues_to_markdown, @@ -21,7 +21,7 @@ strip_prefix, ) from codespy.config import get_settings -from codespy.config_memory import get_memory_store +from codespy.config_memory import get_episode_store from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -392,16 +392,20 @@ async def _review_scope( async with SignatureContext("supply_chain", self._cost_tracker): # Load own prior "supply_chain" episode for this scope scope_initial_memory: ContextMemory | None = None + store = None if self._settings.get_memory_enabled("supply_chain"): - ep = find_latest_episode( - get_memory_store(self._settings), - scope.scope_path(), - task="supply_chain", - exclude_run_id=run_id, - ) - if ep is not None: - scope_initial_memory = ep.context_memory - if self._settings.get_memory_enabled("supply_chain"): + store = get_episode_store(self._settings) + if store is not None: + topic_ids = [scope.topic(review_context.pr_context.repo_full_name).id] + scope_initial_memory = store.load_context( + task="supply_chain", + topic_ids=topic_ids, + ) + if scope_initial_memory: + logger.info("Loaded prior supply_chain episode for scope %s", scope.subroot) + else: + logger.info("No prior supply_chain episode for scope %s", scope.subroot) + if self._settings.get_memory_enabled("supply_chain") and store is not None: question = ( f"review supply chain of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -432,12 +436,10 @@ async def _review_scope( if issue.confidence >= self._settings.min_confidence ] # Fire-and-forget background episode save - _store = get_memory_store(self._settings) - _scope_path = scope.scope_path() _artifacts = {"review": issues_to_markdown(issues)} - def _persist(m=mem, s=_store, p=_scope_path, a=_artifacts): + def _persist(m=mem, s=store, a=_artifacts): try: - m.end_episode(s, p, artifacts=a) + m.end_episode(s, artifacts=a) except Exception: logger.warning("Background supply_chain episode save failed", exc_info=True) submit_episode_save(_persist, name="supply-chain-episode-save") diff --git a/src/codespy/config.py b/src/codespy/config.py index 5b5eaea..3529b08 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -39,7 +39,7 @@ LLMSettings, MemoryConfig, apply_memory_env_overrides, - reset_memory_store, + reset_episode_store, ) if TYPE_CHECKING: @@ -605,5 +605,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() + reset_episode_store() return settings diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index a28457e..4f0810e 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -2,19 +2,20 @@ from __future__ import annotations +import logging import os -from typing import TYPE_CHECKING, Any, Literal + +from typing import TYPE_CHECKING, Any from pydantic import BaseModel, Field from codespy.config_dspy import ReasoningEffort -from codespy.tools.storage.base import Storage if TYPE_CHECKING: + from codespy.agents.memory.postgres import EpisodeStore from codespy.config import Settings - -MemoryBackend = Literal["filesystem", "s3"] +logger = logging.getLogger(__name__) class ReflectionModuleConfig(BaseModel): @@ -63,13 +64,12 @@ class MemoryConfig(BaseModel): ``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) + # PostgreSQL connection settings + postgres_uri: str | None = None # MEMORY_POSTGRES_URI (production) + bank_id: str | None = None # MEMORY_BANK_ID (defaults to "codespy") + pg0_name: str = "codespy" # MEMORY_PG0_NAME (local dev) + pg0_port: int | None = None # MEMORY_PG0_PORT (auto-detected if unset) + pg0_data_dir: str | None = None # MEMORY_PG0_DATA_DIR (custom data directory for pg0) # Reflection defaults — overridable per-signature default_enabled: bool = False # MEMORY_DEFAULT_ENABLED @@ -124,11 +124,11 @@ class MemoryConfig(BaseModel): # ``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", + "POSTGRES_URI": "postgres_uri", + "BANK_ID": "bank_id", + "PG0_NAME": "pg0_name", + "PG0_PORT": "pg0_port", + "PG0_DATA_DIR": "pg0_data_dir", "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", "COMPACT_TRAJECTORY": "compact_trajectory", @@ -157,14 +157,19 @@ class MemoryConfig(BaseModel): REFLECTION_MODULE_PREFIXES = {f"{name.upper()}_": name for name in REFLECTION_MODULES} +def _generate_bank_id() -> str: + """Generate a default bank_id.""" + return "codespy" + + 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_MAX_CONTEXT_MEMORY_TOKENS=512 -> memory.max_context_memory_tokens + MEMORY_POSTGRES_URI=postgresql://localhost/db -> memory.postgres_uri + MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled + MEMORY_MAX_CONTEXT_MEMORY_TOKENS=512 -> memory.max_context_memory_tokens Reflection module overrides use a second level of nesting:: @@ -233,75 +238,82 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: 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 -# there's no reason not to reuse them too. -_store: Storage | None = None +# Cached singleton store. Avoids reconstructing the EpisodeStore's connection pool +# on every call. +_store: EpisodeStore | 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. +def get_episode_store(settings: Settings) -> EpisodeStore | None: + """Return the cached EpisodeStore 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. + for the connection pool setup. - Call :func:`reset_memory_store` after changing settings (e.g. via + Call :func:`reset_episode_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. + Priority: + 1. If ``memory.postgres_uri`` is set, use it directly. + 2. Else, try to auto-start pg0-embedded for local dev. + 3. If pg0 is not available, return None with a warning. Args: settings: Application settings. Returns: - Cached Storage instance, or None if storage is not configured. + Cached EpisodeStore instance, or None if storage is not configured. """ global _store, _store_built if _store_built: return _store mem = settings.memory + bank_id = mem.bank_id or _generate_bank_id() - if mem.backend == "s3": - if not mem.s3_bucket: - _store = None - else: - from codespy.tools.storage.s3.client import S3Client + # Try external PostgreSQL first + if mem.postgres_uri: + from codespy.agents.memory.postgres import EpisodeStore - _store = S3Client( - bucket=mem.s3_bucket, - region=mem.s3_region or settings.aws_region, - endpoint_url=mem.s3_endpoint_url or None, - ) + _store = EpisodeStore(mem.postgres_uri, bank_id) + logger.info(f"EpisodeStore connected to external PostgreSQL (bank={bank_id})") 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) + # Try pg0-embedded for local dev + try: + from codespy.agents.memory.pg0_manager import get_pg0_uri + + uri = get_pg0_uri(name=mem.pg0_name, port=mem.pg0_port, data_dir=mem.pg0_data_dir) + from codespy.agents.memory.postgres import EpisodeStore + + _store = EpisodeStore(uri, bank_id) + logger.info(f"EpisodeStore connected to pg0-embedded PostgreSQL (bank={bank_id})") + except ImportError: + logger.warning( + "Memory is enabled but no PostgreSQL URI is configured and pg0-embedded " + "is not installed. Install with: pip install pg0-embedded\n" + "Or set MEMORY_POSTGRES_URI to use an external PostgreSQL instance." + ) + _store = None + except Exception as e: + logger.warning(f"Failed to start pg0-embedded: {e}") + _store = None _store_built = True return _store -def reset_memory_store() -> None: +def reset_episode_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 + if _store is not None: + try: + _store.close() + except Exception: + pass _store = None _store_built = False @@ -322,13 +334,16 @@ def verify_memory_access(settings: Settings) -> tuple[bool, str]: ): return True, "Memory disabled — skipping storage check" - store = get_memory_store(settings) + store = get_episode_store(settings) if store is None: - return False, "Memory is enabled but storage is not configured (missing S3 bucket?)" + return ( + False, + "Memory is enabled but storage is not configured (set MEMORY_POSTGRES_URI or install pg0-embedded)", + ) 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})" + return True, f"Memory storage verified (PostgreSQL, bank={settings.memory.bank_id or _generate_bank_id()})" diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py index 33488d1..8d7d030 100644 --- a/tests/test_config_memory.py +++ b/tests/test_config_memory.py @@ -1,13 +1,163 @@ -"""Tests for memory storage access verification.""" +"""Tests for memory storage configuration and access verification.""" -import sys from unittest.mock import MagicMock, patch import pytest -from codespy.config_memory import verify_memory_access -from codespy.tools.storage.filesystem.client import FileSystem -from codespy.tools.storage.s3.client import S3Client +from codespy.config_memory import ( + _generate_bank_id, + apply_memory_env_overrides, + get_episode_store, + reset_episode_store, + verify_memory_access, +) + + +class TestGenerateBankId: + """Tests for _generate_bank_id function.""" + + def test_generate_bank_id_returns_codespy(self): + """Should return 'codespy' as the default bank_id.""" + result = _generate_bank_id() + assert result == "codespy" + + +class TestApplyMemoryEnvOverrides: + """Tests for apply_memory_env_overrides function.""" + + def test_override_postgres_uri(self, monkeypatch): + """MEMORY_POSTGRES_URI should set memory.postgres_uri.""" + monkeypatch.setenv("MEMORY_POSTGRES_URI", "postgresql://localhost:5432/test") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres_uri"] == "postgresql://localhost:5432/test" + + def test_override_bank_id(self, monkeypatch): + """MEMORY_BANK_ID should set memory.bank_id.""" + monkeypatch.setenv("MEMORY_BANK_ID", "my-agent") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["bank_id"] == "my-agent" + + def test_override_pg0_name(self, monkeypatch): + """MEMORY_PG0_NAME should set memory.pg0_name.""" + monkeypatch.setenv("MEMORY_PG0_NAME", "custom_name") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["pg0_name"] == "custom_name" + + def test_override_pg0_port(self, monkeypatch): + """MEMORY_PG0_PORT should set memory.pg0_port as int.""" + monkeypatch.setenv("MEMORY_PG0_PORT", "5433") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["pg0_port"] == 5433 + + def test_override_default_enabled(self, monkeypatch): + """MEMORY_DEFAULT_ENABLED should set memory.default_enabled as bool.""" + monkeypatch.setenv("MEMORY_DEFAULT_ENABLED", "true") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["default_enabled"] is True + + def test_override_reflection_module(self, monkeypatch): + """MEMORY_DISTILLER_MODEL should set memory.distiller.model.""" + monkeypatch.setenv("MEMORY_DISTILLER_MODEL", "claude-3-sonnet") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["distiller"]["model"] == "claude-3-sonnet" + + def test_no_memory_prefix_ignored(self, monkeypatch): + """Non-MEMORY_ env vars should be ignored.""" + monkeypatch.setenv("OTHER_POSTGRES_URI", "postgresql://localhost/db") + config = {} + result = apply_memory_env_overrides(config) + assert "memory" not in result or "postgres_uri" not in result.get("memory", {}) + + +class TestGetEpisodeStore: + """Tests for get_episode_store function.""" + + def test_returns_none_when_no_config(self): + """Should return None when postgres_uri not set and pg0 not available.""" + settings = MagicMock() + settings.memory.postgres_uri = None + settings.memory.bank_id = "test-bank" + settings.memory.pg0_name = "codespy" + settings.memory.pg0_port = None + + # Simulate pg0 not being available + with patch("codespy.config_memory._store", None): + with patch("codespy.config_memory._store_built", False): + with patch( + "codespy.agents.memory.pg0_manager.get_pg0_uri", + side_effect=ImportError("pg0 not installed"), + ): + result = get_episode_store(settings) + + assert result is None + + def test_uses_postgres_uri_when_set(self): + """Should use external PostgreSQL when MEMORY_POSTGRES_URI is set.""" + settings = MagicMock() + settings.memory.postgres_uri = "postgresql://localhost:5432/codespy" + settings.memory.bank_id = "test-bank" + + mock_store = MagicMock() + + with patch("codespy.config_memory._store", None): + with patch("codespy.config_memory._store_built", False): + with patch( + "codespy.agents.memory.postgres.EpisodeStore", + return_value=mock_store, + ) as mock_episode_store: + result = get_episode_store(settings) + + mock_episode_store.assert_called_once_with( + "postgresql://localhost:5432/codespy", "test-bank" + ) + assert result == mock_store + + def test_caching_behavior(self): + """Should cache the store after first call.""" + settings = MagicMock() + settings.memory.postgres_uri = "postgresql://localhost:5432/codespy" + settings.memory.bank_id = "test-bank" + + mock_store = MagicMock() + + with patch("codespy.config_memory._store", mock_store): + with patch("codespy.config_memory._store_built", True): + result = get_episode_store(settings) + + # Should return cached store without creating new one + assert result == mock_store + + +class TestResetEpisodeStore: + """Tests for reset_episode_store function.""" + + def test_closes_existing_store(self): + """Should close existing store and reset cache.""" + mock_store = MagicMock() + + with patch("codespy.config_memory._store", mock_store): + with patch("codespy.config_memory._store_built", True): + reset_episode_store() + + mock_store.close.assert_called_once() + + def test_handles_close_exception(self): + """Should handle exceptions during close gracefully.""" + mock_store = MagicMock() + mock_store.close.side_effect = Exception("Close failed") + + with patch("codespy.config_memory._store", mock_store): + with patch("codespy.config_memory._store_built", True): + # Should not raise + reset_episode_store() + + mock_store.close.assert_called_once() class TestVerifyMemoryAccess: @@ -43,9 +193,8 @@ def memory_enabled(sig): assert "Memory disabled" in message def test_verify_memory_access_store_none(self): - """When memory active but store is None (missing S3 bucket).""" + """When memory active but store is None (no PostgreSQL configured).""" settings = MagicMock() - settings.memory.backend = "s3" def is_enabled(sig): return sig == "summary" # Only summary enabled @@ -56,16 +205,16 @@ def memory_enabled(sig): 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): + with patch("codespy.config_memory.get_episode_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.""" + def test_verify_memory_access_postgres_ok(self): + """When memory active with valid PostgreSQL store.""" settings = MagicMock() - settings.memory.backend = "filesystem" + settings.memory.bank_id = "test-bank" def is_enabled(sig): return sig == "summary" @@ -76,46 +225,22 @@ def memory_enabled(sig): 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 EpisodeStore 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): + with patch("codespy.config_memory.get_episode_store", return_value=mock_store): success, message = verify_memory_access(settings) assert success is True assert "verified" in message - assert "s3" in message + assert "PostgreSQL" 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" + settings.memory.bank_id = "test-bank" def is_enabled(sig): return sig == "summary" @@ -128,90 +253,11 @@ def memory_enabled(sig): # Mock store that raises on verify_access mock_store = MagicMock() - mock_store.verify_access.side_effect = PermissionError("Access denied") + mock_store.verify_access.side_effect = ConnectionError("Cannot connect") - with patch("codespy.config_memory.get_memory_store", return_value=mock_store): + with patch("codespy.config_memory.get_episode_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() + assert "Cannot connect" in message diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index 5451966..d945cbc 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -272,8 +272,8 @@ 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.""" + def test_replace_nonexistent_valid_prefix_falls_back_to_add(self, caplog): + """REPLACE on non-existent item with valid prefix falls back to ADD.""" import logging memory = ContextMemory( @@ -282,16 +282,55 @@ def test_replace_nonexistent_logs_warning(self, caplog): ], ) ops = [Operation(type=OpType.REPLACE, item_id="cu-GONE", content="New content")] + with caplog.at_level( + logging.INFO, + logger="codespy.agents.memory.hippocampus.context_memory", + ): + new_memory, new_ids = memory.apply(ops, topic_ids=["t2"]) + + assert len(new_ids) == 1 # fallback ADD created a new item + assert new_memory.context_understanding[0].content == "Existing" # original untouched + assert len(new_memory.context_understanding) == 2 # original + fallback + fallback_item = new_memory.context_understanding[1] + assert fallback_item.content == "New content" + assert fallback_item.topic_ids == ["t2"] # gets current topic_ids + assert fallback_item.id.startswith("cu-") # correct prefix + assert "falling back to ADD" in caplog.text + + def test_replace_topic_id_skipped(self, caplog): + """REPLACE with topic-ID-shaped item_id (no prefix) logs warning and skips.""" + import logging + + memory = ContextMemory( + context_understanding=[ + Item(id="cu-abc", content="Existing", topic_ids=["t1"]), + ], + ) + ops = [Operation(type=OpType.REPLACE, item_id="owner/repo/package", content="New")] 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 == [] # no fallback ADD + assert len(new_memory.context_understanding) == 1 # unchanged + assert "no valid prefix" in caplog.text + + def test_replace_url_topic_id_skipped(self, caplog): + """REPLACE with PR URL as item_id logs warning and skips.""" + import logging + + memory = ContextMemory() + ops = [Operation(type=OpType.REPLACE, item_id="https://github.com/o/r/pull/1", content="X")] + with caplog.at_level( + logging.WARNING, + logger="codespy.agents.memory.hippocampus.context_memory", + ): + new_memory, new_ids = memory.apply(ops) + assert new_ids == [] - assert new_memory.context_understanding[0].content == "Existing" - assert "cu-GONE" in caplog.text - assert "not found" in caplog.text + assert "no valid prefix" in caplog.text class TestContextMemoryMerge: diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py index fad437e..29d8486 100644 --- a/tests/test_hippocampus.py +++ b/tests/test_hippocampus.py @@ -1,15 +1,453 @@ -"""Tests for Hippocampus internal methods.""" +"""Tests for Hippocampus with PostgreSQL storage (pg0-embedded).""" import pytest +from datetime import UTC, datetime + +# Skip all tests in this file if pg0-embedded is not installed +try: + import pg0 + PG0_AVAILABLE = True +except ImportError: + PG0_AVAILABLE = False from codespy.agents.memory.hippocampus import ( ContextMemory, Hippocampus, Item, - ItemTag, + Mutation, Operation, OpType, + Topic, ) +from codespy.agents.memory.hippocampus.episode import Episode +from codespy.agents.memory.postgres import EpisodeStore + + +pytestmark = pytest.mark.skipif( + not PG0_AVAILABLE, + reason="pg0-embedded not installed (pip install pg0-embedded)" +) + + +@pytest.fixture(scope="module") +def pg0_uri(): + """Create a pg0-embedded PostgreSQL instance for testing.""" + from codespy.agents.memory.pg0_manager import get_pg0_uri, stop_pg0 + + uri = get_pg0_uri(name="codespy_test", port=None) + yield uri + stop_pg0() + + +@pytest.fixture +def episode_store(pg0_uri): + """Create a fresh EpisodeStore for each test.""" + store = EpisodeStore(pg0_uri, bank_id="test_bank") + yield store + store.close() + + +class TestEpisodeStoreBasics: + """Basic tests for EpisodeStore initialization and schema.""" + + def test_episode_store_initializes(self, episode_store): + """EpisodeStore should initialize and create schema.""" + assert episode_store is not None + assert episode_store.bank_id == "test_bank" + + def test_verify_access_succeeds(self, episode_store): + """verify_access should succeed after initialization.""" + # Should not raise + episode_store.verify_access() + + +class TestEpisodeStoreSaveLoad: + """Tests for saving and loading episodes.""" + + def test_save_simple_episode(self, episode_store): + """Should save a simple episode with context memory.""" + # Create a simple episode + ctx = ContextMemory( + topics=[Topic(id="test/topic", description="Test topic")], + context_understanding=[ + Item(id="cu-1", content="Test item", topic_ids=["test/topic"]) + ], + ) + episode = Episode( + id=__import__("uuid").uuid4(), + task="test_task", + module="TestModule", + question="Test question", + context_memory=ctx, + timestamp=datetime.now(UTC), + run_id="test-run-123", + mutations=[], + artifacts={"test": "artifact"}, + ) + + # Save should not raise + episode_store.save_episode(episode) + + def test_load_context_returns_none_for_no_matching_episode(self, episode_store): + """load_context should return None when no matching episode exists.""" + result = episode_store.load_context( + task="nonexistent_task", + topic_ids=["nonexistent/topic"], + ) + assert result is None + + def test_save_and_load_episode_roundtrip(self, episode_store): + """Should save and load an episode successfully.""" + import uuid + + # Create and save an episode + ctx = ContextMemory( + topics=[Topic(id="owner/repo/pkg", description="Test package")], + context_understanding=[ + Item(id="cu-abc123", content="Test understanding", topic_ids=["owner/repo/pkg"]) + ], + ) + episode = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="Review PR #123", + context_memory=ctx, + timestamp=datetime.now(UTC), + run_id="run-456", + mutations=[], + artifacts={"review": "LGTM"}, + ) + episode_store.save_episode(episode) + + # Load context for the same task and topic + loaded_ctx = episode_store.load_context( + task="code_review", + topic_ids=["owner/repo/pkg"], + ) + + # Should have loaded the context + assert loaded_ctx is not None + assert len(loaded_ctx.topics) == 1 + assert loaded_ctx.topics[0].id == "owner/repo/pkg" + assert len(loaded_ctx.context_understanding) == 1 + assert loaded_ctx.context_understanding[0].content == "Test understanding" + + +class TestEpisodeStoreItemVersioning: + """Tests for item versioning on REPLACE operations.""" + + def test_item_versioning_on_replace(self, episode_store): + """Item versions should increment on REPLACE operations.""" + import uuid + + topic_id = "owner/repo" + + # Episode 1: Add an item + ctx1 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id="cu-item1", content="Original content", topic_ids=[topic_id]) + ], + ) + episode1 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="First review", + context_memory=ctx1, + timestamp=datetime.now(UTC), + run_id="run-1", + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + item_id="cu-item1", + section="context_understanding", + content="Original content", + previous_content=None, + topic_ids=[topic_id], + ) + ], + artifacts={}, + ) + episode_store.save_episode(episode1) + + # Episode 2: Replace the item + ctx2 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id="cu-item1", content="Updated content", topic_ids=[topic_id]) + ], + ) + episode2 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="Second review", + context_memory=ctx2, + timestamp=datetime.now(UTC), + run_id="run-2", + mutations=[ + Mutation( + step=0, + type=OpType.REPLACE, + item_id="cu-item1", + section="context_understanding", + content="Updated content", + previous_content="Original content", + topic_ids=[topic_id], + ) + ], + artifacts={}, + ) + episode_store.save_episode(episode2) + + # Load the latest context + loaded_ctx = episode_store.load_context( + task="code_review", + topic_ids=[topic_id], + ) + + # Should have the updated content + assert loaded_ctx is not None + assert loaded_ctx.context_understanding[0].content == "Updated content" + + def test_inherited_items_preserved(self, episode_store): + """Inherited items (no mutation) should preserve their content.""" + import uuid + + topic_id = "owner/repo" + + # Episode 1: Add two items + ctx1 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id="cu-item1", content="Item 1 content", topic_ids=[topic_id]), + Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), + ], + ) + episode1 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="First review", + context_memory=ctx1, + timestamp=datetime.now(UTC), + run_id="run-1", + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + item_id="cu-item1", + section="context_understanding", + content="Item 1 content", + previous_content=None, + topic_ids=[topic_id], + ), + Mutation( + step=0, + type=OpType.ADD, + item_id="cu-item2", + section="context_understanding", + content="Item 2 content", + previous_content=None, + topic_ids=[topic_id], + ), + ], + artifacts={}, + ) + episode_store.save_episode(episode1) + + # Episode 2: Only replace item1, item2 is inherited + ctx2 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id="cu-item1", content="Item 1 updated", topic_ids=[topic_id]), + Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), # inherited + ], + ) + episode2 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="Second review", + context_memory=ctx2, + timestamp=datetime.now(UTC), + run_id="run-2", + mutations=[ + Mutation( + step=0, + type=OpType.REPLACE, + item_id="cu-item1", + section="context_understanding", + content="Item 1 updated", + previous_content="Item 1 content", + topic_ids=[topic_id], + ), + ], + artifacts={}, + ) + episode_store.save_episode(episode2) + + # Load the latest context + loaded_ctx = episode_store.load_context( + task="code_review", + topic_ids=[topic_id], + ) + + # Both items should be present with correct content + assert loaded_ctx is not None + items_by_id = {item.id: item for item in loaded_ctx.context_understanding} + assert items_by_id["cu-item1"].content == "Item 1 updated" + assert items_by_id["cu-item2"].content == "Item 2 content" + + +class TestEpisodeStoreWithHippocampus: + """Integration tests for Hippocampus with EpisodeStore.""" + + def test_hippocampus_end_episode_with_store(self, episode_store): + """Hippocampus.end_episode should work with EpisodeStore.""" + import dspy + + # Create a simple mock agent + class MockAgent(dspy.Module): + def forward(self, **kwargs): + return dspy.Prediction(result="test") + + # Create Hippocampus with the mock agent + mem = Hippocampus( + MockAgent(), + task_name="test_task", + run_id="test-run", + topics=[Topic(id="test/topic", description="Test topic")], + ) + + # Make a call + mem.forward() + + # End episode with the store + mem.end_episode(store=episode_store, artifacts={"test": "value"}) + + # Episode should be set + assert mem.episode is not None + assert mem.episode.task == "test_task" + assert mem.episode.run_id == "test-run" + + def test_load_context_returns_latest_episode(self, episode_store): + """load_context should return the context from the latest episode.""" + import uuid + + topic_id = "owner/repo" + + # Save multiple episodes + for i in range(3): + ctx = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id=f"cu-item{i}", content=f"Content {i}", topic_ids=[topic_id]) + ], + ) + episode = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question=f"Review {i}", + context_memory=ctx, + timestamp=datetime.now(UTC), + run_id=f"run-{i}", + mutations=[], + artifacts={}, + ) + episode_store.save_episode(episode) + + # Load the latest context + loaded_ctx = episode_store.load_context( + task="code_review", + topic_ids=[topic_id], + ) + + # Should have the latest content (from episode 2) + assert loaded_ctx is not None + assert len(loaded_ctx.context_understanding) == 3 # All items accumulated + + +class TestEpisodeStoreDeleteTombstone: + """Tests for DELETE operations (tombstone handling).""" + + def test_delete_creates_tombstone(self, episode_store): + """DELETE should create a tombstone version of the item.""" + import uuid + + topic_id = "owner/repo" + + # Episode 1: Add an item + ctx1 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[ + Item(id="cu-item1", content="To be deleted", topic_ids=[topic_id]) + ], + ) + episode1 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="First review", + context_memory=ctx1, + timestamp=datetime.now(UTC), + run_id="run-1", + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + item_id="cu-item1", + section="context_understanding", + content="To be deleted", + previous_content=None, + topic_ids=[topic_id], + ) + ], + artifacts={}, + ) + episode_store.save_episode(episode1) + + # Episode 2: Delete the item (item not in context_memory, but in mutations) + ctx2 = ContextMemory( + topics=[Topic(id=topic_id, description="Test repo")], + context_understanding=[], # Empty after delete + ) + episode2 = Episode( + id=uuid.uuid4(), + task="code_review", + module="CodeReviewer", + question="Second review", + context_memory=ctx2, + timestamp=datetime.now(UTC), + run_id="run-2", + mutations=[ + Mutation( + step=0, + type=OpType.DELETE, + item_id="cu-item1", + section="context_understanding", + content=None, # DELETE has no new content + previous_content="To be deleted", + topic_ids=[topic_id], + ) + ], + artifacts={}, + ) + episode_store.save_episode(episode2) + + # Load the latest context - deleted item should not appear + loaded_ctx = episode_store.load_context( + task="code_review", + topic_ids=[topic_id], + ) + + # Should have no items (deleted) + assert loaded_ctx is not None + assert len(loaded_ctx.context_understanding) == 0 class TestRecordMutations: @@ -103,33 +541,39 @@ def _make_hip(): def test_helpful_increments(self): hip = self._make_hip() + from codespy.agents.memory.hippocampus import ItemTag hip._update_item_scores({"a": ItemTag.HELPFUL}) assert hip.scores == {"a": 1} def test_helpful_accumulates(self): hip = self._make_hip() + from codespy.agents.memory.hippocampus import ItemTag 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() + from codespy.agents.memory.hippocampus import ItemTag 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() + from codespy.agents.memory.hippocampus import ItemTag hip._update_item_scores({"a": ItemTag.STALE}) assert hip.scores == {"a": -1} def test_neutral_initializes_zero(self): hip = self._make_hip() + from codespy.agents.memory.hippocampus import ItemTag hip._update_item_scores({"a": ItemTag.NEUTRAL}) assert hip.scores == {"a": 0} def test_neutral_preserves_existing(self): hip = self._make_hip() + from codespy.agents.memory.hippocampus import ItemTag hip.scores = {"a": 5} hip._update_item_scores({"a": ItemTag.NEUTRAL}) assert hip.scores == {"a": 5} From 442f820321497471100e97825c669f7873a4e1f7 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Tue, 8 Sep 2026 15:24:33 +0200 Subject: [PATCH 05/14] readme --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 4da5274..79f47ed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -112,6 +112,7 @@ AUTO_DISCOVER_GEMINI=false | Temperature | `DEFAULT_TEMPERATURE` | `0.2` | Default temperature for LLM calls | | Max iterations | `DEFAULT_MAX_ITERS` | `5` | Maximum ReAct iterations for tool-using agents | | Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) | +| Compact patches | `COMPACT_PATCHES` | `false` | Expand diff hunks to full function bodies using Tree-sitter | | RLM fallback | `RLM_FALLBACK_ENABLED` | `true` | Proactive RLM fallback for context rot prevention | | RLM react threshold | `RLM_FALLBACK_REACT_THRESHOLD` | `0.30` | Context ratio triggering RLM for ReAct modules | | RLM CoT threshold | `RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD` | `0.40` | Context ratio triggering RLM for ChainOfThought modules | From a4b20fa9b1ab16fb9c4cbc34655192f36be1b850 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Wed, 9 Sep 2026 22:14:15 +0200 Subject: [PATCH 06/14] sql --- .../memory/hippocampus/context_memory.py | 12 +- src/codespy/agents/memory/pg0_manager.py | 2 +- src/codespy/agents/memory/postgres.py | 259 ++++++++---------- src/codespy/agents/reviewer/models.py | 5 +- .../agents/reviewer/modules/auditor.py | 5 +- .../agents/reviewer/modules/scope_resolver.py | 25 +- .../agents/reviewer/modules/summarizer.py | 5 +- tests/test_context_memory.py | 16 +- tests/test_hippocampus.py | 20 +- 9 files changed, 146 insertions(+), 203 deletions(-) diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 9d37f8c..72b1585 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -75,13 +75,17 @@ def _missing_(cls, value: object) -> OpType | None: class Topic(BaseModel): - """A topic representing a scope in the repository. + """A named entity that scopes context observations. - Topics are used to group context items by their relevant scope. + Topics group observations by domain — a project component, a customer, + a pull request, an external service, or any logical boundary the caller + defines. The `type` field discriminates the kind of entity while `id` + uniquely identifies the instance within that kind. """ - id: str = Field(description="Topic identifier (e.g., 'owner/repo/package-name')") - description: str = Field(description="Description of this topic's role") + id: str = Field(description="Unique topic identifier within its type") + type: str = Field(description="Topic kind, e.g. 'project_scope', 'pull_request', 'customer'") + description: str = Field(description="Human-readable description of this topic") class Item(BaseModel): diff --git a/src/codespy/agents/memory/pg0_manager.py b/src/codespy/agents/memory/pg0_manager.py index d983e6f..5fde995 100644 --- a/src/codespy/agents/memory/pg0_manager.py +++ b/src/codespy/agents/memory/pg0_manager.py @@ -26,7 +26,7 @@ def _wait_for_ready( The pg0 binary's ``info`` command runs ``psql -c 'SELECT 1'`` internally and only returns a URI when the healthcheck passes. Polling this accounts for slow first-run initialization (initdb, - pgvector installation) without reimplementing the healthcheck. + extensions setup) without reimplementing the healthcheck. Returns: Connection URI string, or None on timeout. diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index a410352..bdf9e67 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -63,7 +63,6 @@ def ensure_schema(self) -> None: with self._pool.connection() as conn: with conn.cursor() as cur: # Extensions - cur.execute("CREATE EXTENSION IF NOT EXISTS vector") cur.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm") # Schema version table @@ -86,23 +85,12 @@ def ensure_schema(self) -> None: cur.execute(""" CREATE TABLE IF NOT EXISTS topics ( bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, - id VARCHAR(512) NOT NULL, + id VARCHAR(256) NOT NULL, + type VARCHAR(64) NOT NULL, description TEXT NOT NULL, - description_embedding vector(1536), - description_tsv tsvector GENERATED ALWAYS AS ( - to_tsvector('english', coalesce(description, '')) - ) STORED, PRIMARY KEY (bank_id, id) ) """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_topics_desc_embedding - ON topics USING hnsw (description_embedding vector_cosine_ops) - """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_topics_desc_tsv - ON topics USING gin (description_tsv) - """) cur.execute(""" CREATE INDEX IF NOT EXISTS idx_topics_id_trgm ON topics USING gin (id gin_trgm_ops) @@ -118,10 +106,6 @@ def ensure_schema(self) -> None: module VARCHAR(64) NOT NULL, question TEXT NOT NULL DEFAULT '', timestamp TIMESTAMPTZ NOT NULL, - question_embedding vector(1536), - question_tsv tsvector GENERATED ALWAYS AS ( - to_tsvector('english', coalesce(question, '')) - ) STORED, PRIMARY KEY (bank_id, id) ) """) @@ -129,21 +113,13 @@ def ensure_schema(self) -> None: CREATE INDEX IF NOT EXISTS idx_episodes_task_time ON episodes (bank_id, task, timestamp DESC) """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_episodes_question_embedding - ON episodes USING hnsw (question_embedding vector_cosine_ops) - """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_episodes_question_tsv - ON episodes USING gin (question_tsv) - """) # Episode topics junction cur.execute(""" CREATE TABLE IF NOT EXISTS episode_topics ( bank_id VARCHAR(64) NOT NULL, episode_id UUID NOT NULL, - topic_id VARCHAR(512) NOT NULL, + topic_id VARCHAR(256) NOT NULL, PRIMARY KEY (bank_id, episode_id, topic_id), FOREIGN KEY (bank_id, episode_id) REFERENCES episodes(bank_id, id) ON DELETE CASCADE, @@ -156,79 +132,67 @@ def ensure_schema(self) -> None: ON episode_topics (bank_id, topic_id) """) - # Items table with flattened mutation fields + # Observations table with flattened mutation fields cur.execute(""" - CREATE TABLE IF NOT EXISTS items ( + CREATE TABLE IF NOT EXISTS observations ( bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, id VARCHAR(48) NOT NULL, version INT NOT NULL DEFAULT 1, - section VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, content TEXT, episode_id UUID NOT NULL, step INT NOT NULL DEFAULT 0, op_type VARCHAR(8) NOT NULL, previous_content TEXT, ordinal INT NOT NULL DEFAULT 0, - content_embedding vector(1536), - content_tsv tsvector GENERATED ALWAYS AS ( - to_tsvector('english', coalesce(content, '')) - ) STORED, PRIMARY KEY (bank_id, id, version), FOREIGN KEY (bank_id, episode_id) REFERENCES episodes(bank_id, id) ON DELETE CASCADE ) """) cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_items_episode - ON items (bank_id, episode_id) - """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_items_content_embedding - ON items USING hnsw (content_embedding vector_cosine_ops) - """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_items_content_tsv - ON items USING gin (content_tsv) + CREATE INDEX IF NOT EXISTS idx_observations_episode + ON observations (bank_id, episode_id) """) - # Item topics junction + # Observation topics junction cur.execute(""" - CREATE TABLE IF NOT EXISTS item_topics ( + CREATE TABLE IF NOT EXISTS observation_topics ( bank_id VARCHAR(64) NOT NULL, - item_id VARCHAR(48) NOT NULL, - item_version INT NOT NULL, - topic_id VARCHAR(512) NOT NULL, - item_occurrence INT NOT NULL DEFAULT 0, + observation_id VARCHAR(48) NOT NULL, + observation_version INT NOT NULL, + topic_id VARCHAR(256) NOT NULL, + observation_occurrence INT NOT NULL DEFAULT 0, version_occurrence INT NOT NULL DEFAULT 0, - PRIMARY KEY (bank_id, item_id, item_version, topic_id), - FOREIGN KEY (bank_id, item_id, item_version) - REFERENCES items(bank_id, id, version) ON DELETE CASCADE, + PRIMARY KEY (bank_id, observation_id, observation_version, topic_id), + FOREIGN KEY (bank_id, observation_id, observation_version) + REFERENCES observations(bank_id, id, version) ON DELETE CASCADE, FOREIGN KEY (bank_id, topic_id) REFERENCES topics(bank_id, id) ON DELETE CASCADE ) """) cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_item_topics_reverse - ON item_topics (bank_id, topic_id) + CREATE INDEX IF NOT EXISTS idx_observation_topics_reverse + ON observation_topics (bank_id, topic_id) """) - # Episode items junction + # Episode observations junction cur.execute(""" - CREATE TABLE IF NOT EXISTS episode_items ( + CREATE TABLE IF NOT EXISTS episode_observations ( bank_id VARCHAR(64) NOT NULL, episode_id UUID NOT NULL, - item_id VARCHAR(48) NOT NULL, - item_version INT NOT NULL, - PRIMARY KEY (bank_id, episode_id, item_id), + observation_id VARCHAR(48) NOT NULL, + observation_version INT NOT NULL, + PRIMARY KEY (bank_id, episode_id, observation_id), FOREIGN KEY (bank_id, episode_id) REFERENCES episodes(bank_id, id) ON DELETE CASCADE, - FOREIGN KEY (bank_id, item_id, item_version) - REFERENCES items(bank_id, id, version) ON DELETE CASCADE + FOREIGN KEY (bank_id, observation_id, observation_version) + REFERENCES observations(bank_id, id, version) ON DELETE CASCADE ) """) cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_episode_items_reverse - ON episode_items (bank_id, item_id) + CREATE INDEX IF NOT EXISTS idx_episode_observations_reverse + ON episode_observations (bank_id, observation_id) """) # Artifacts table @@ -238,18 +202,11 @@ def ensure_schema(self) -> None: episode_id UUID NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL, - content_tsv tsvector GENERATED ALWAYS AS ( - to_tsvector('english', coalesce(content, '')) - ) STORED, PRIMARY KEY (bank_id, episode_id, name), FOREIGN KEY (bank_id, episode_id) REFERENCES episodes(bank_id, id) ON DELETE CASCADE ) """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_artifacts_content_tsv - ON artifacts USING gin (content_tsv) - """) conn.commit() @@ -309,11 +266,11 @@ def save_episode(self, episode: Episode) -> None: topic_ids.add(topic.id) cur.execute( """ - INSERT INTO topics (bank_id, id, description) - VALUES (%s, %s, %s) - ON CONFLICT (bank_id, id) DO UPDATE SET description = EXCLUDED.description + INSERT INTO topics (bank_id, id, type, description) + VALUES (%s, %s, %s, %s) + ON CONFLICT (bank_id, id) DO UPDATE SET type = EXCLUDED.type, description = EXCLUDED.description """, - (self.bank_id, topic.id, topic.description), + (self.bank_id, topic.id, topic.type, topic.description), ) # 4. Insert episode_topics junctions @@ -327,23 +284,23 @@ def save_episode(self, episode: Episode) -> None: (self.bank_id, str(episode.id), topic.id), ) - # 5. Process items and mutations + # 5. Process observations and mutations # Build map of item_id -> mutation for quick lookup mutation_map: dict[str, Mutation] = {} for mut in episode.mutations: mutation_map[mut.item_id] = mut - # Get all current items from context_memory - current_items: dict[str, tuple[str, Item]] = {} # item_id -> (section, item) + # Get all current observations from context_memory + current_items: dict[str, tuple[str, Item]] = {} # observation_id -> (type, observation) for sec in episode.context_memory.section_names(): for item in getattr(episode.context_memory, sec): current_items[item.id] = (sec, item) - # Track items we've processed to detect DELETEs + # Track observations we've processed to detect DELETEs processed_item_ids: set[str] = set() - # Process items in current context memory - for item_id, (section, item) in current_items.items(): + # Process observations in current context memory + for item_id, (type, item) in current_items.items(): processed_item_ids.add(item_id) mutation = mutation_map.get(item_id) @@ -361,11 +318,11 @@ def save_episode(self, episode: Episode) -> None: elif mutation.type == OpType.ADD: op_type = "ADD" else: - # Inherited item - need to look up existing version + # Inherited observation - need to look up existing version cur.execute( """ SELECT MAX(version) as max_ver - FROM items + FROM observations WHERE bank_id = %s AND id = %s """, (self.bank_id, item_id), @@ -375,13 +332,13 @@ def save_episode(self, episode: Episode) -> None: version = row["max_ver"] op_type = "REPLACE" # Already exists - # Insert new item version only if it's ADD or REPLACE + # Insert new observation version only if it's ADD or REPLACE if mutation and mutation.type in (OpType.ADD, OpType.REPLACE): # Get next version number cur.execute( """ SELECT COALESCE(MAX(version), 0) + 1 as next_ver - FROM items + FROM observations WHERE bank_id = %s AND id = %s """, (self.bank_id, item_id), @@ -394,15 +351,15 @@ def save_episode(self, episode: Episode) -> None: cur.execute( """ - INSERT INTO items - (bank_id, id, version, section, content, episode_id, step, op_type, previous_content, ordinal) + INSERT INTO observations + (bank_id, id, version, type, content, episode_id, step, op_type, previous_content, ordinal) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( self.bank_id, item_id, version, - section, + type, item.content, str(episode.id), step, @@ -412,63 +369,63 @@ def save_episode(self, episode: Episode) -> None: ), ) - # Insert episode_items junction + # Insert episode_observations junction cur.execute( """ - INSERT INTO episode_items (bank_id, episode_id, item_id, item_version) + INSERT INTO episode_observations (bank_id, episode_id, observation_id, observation_version) VALUES (%s, %s, %s, %s) - ON CONFLICT (bank_id, episode_id, item_id) DO NOTHING + ON CONFLICT (bank_id, episode_id, observation_id) DO NOTHING """, (self.bank_id, str(episode.id), item_id, version), ) - # Insert item_topics for this version + # Insert observation_topics for this version for topic_id in item.topic_ids: # Count occurrences cur.execute( """ - SELECT item_occurrence, version_occurrence - FROM item_topics - WHERE bank_id = %s AND item_id = %s AND topic_id = %s - ORDER BY item_version DESC + SELECT observation_occurrence, version_occurrence + FROM observation_topics + WHERE bank_id = %s AND observation_id = %s AND topic_id = %s + ORDER BY observation_version DESC LIMIT 1 """, (self.bank_id, item_id, topic_id), ) prev_row = cur.fetchone() if prev_row: - item_occ = prev_row["item_occurrence"] + 1 + observation_occ = prev_row["observation_occurrence"] + 1 ver_occ = ( prev_row["version_occurrence"] + 1 if is_new else 1 ) else: - item_occ = 1 + observation_occ = 1 ver_occ = 1 cur.execute( """ - INSERT INTO item_topics - (bank_id, item_id, item_version, topic_id, item_occurrence, version_occurrence) + INSERT INTO observation_topics + (bank_id, observation_id, observation_version, topic_id, observation_occurrence, version_occurrence) VALUES (%s, %s, %s, %s, %s, %s) - ON CONFLICT (bank_id, item_id, item_version, topic_id) DO NOTHING + ON CONFLICT (bank_id, observation_id, observation_version, topic_id) DO NOTHING """, ( self.bank_id, item_id, version, topic_id, - item_occ, + observation_occ, ver_occ, ), ) else: - # Inherited item - use existing version + # Inherited observation - use existing version cur.execute( """ SELECT MAX(version) as max_ver - FROM items + FROM observations WHERE bank_id = %s AND id = %s """, (self.bank_id, item_id), @@ -476,12 +433,12 @@ def save_episode(self, episode: Episode) -> None: row = cur.fetchone() existing_version = row["max_ver"] if row and row["max_ver"] else 1 - # Insert episode_items junction with existing version + # Insert episode_observations junction with existing version cur.execute( """ - INSERT INTO episode_items (bank_id, episode_id, item_id, item_version) + INSERT INTO episode_observations (bank_id, episode_id, observation_id, observation_version) VALUES (%s, %s, %s, %s) - ON CONFLICT (bank_id, episode_id, item_id) DO NOTHING + ON CONFLICT (bank_id, episode_id, observation_id) DO NOTHING """, ( self.bank_id, @@ -491,26 +448,26 @@ def save_episode(self, episode: Episode) -> None: ), ) - # Increment item_occurrence for inherited items + # Increment observation_occurrence for inherited observations for topic_id in item.topic_ids: cur.execute( """ - SELECT item_occurrence - FROM item_topics - WHERE bank_id = %s AND item_id = %s AND topic_id = %s - AND item_version = %s + SELECT observation_occurrence + FROM observation_topics + WHERE bank_id = %s AND observation_id = %s AND topic_id = %s + AND observation_version = %s """, (self.bank_id, item_id, topic_id, existing_version), ) occ_row = cur.fetchone() if occ_row: - new_occ = occ_row["item_occurrence"] + 1 + new_occ = occ_row["observation_occurrence"] + 1 cur.execute( """ - UPDATE item_topics - SET item_occurrence = %s - WHERE bank_id = %s AND item_id = %s AND topic_id = %s - AND item_version = %s + UPDATE observation_topics + SET observation_occurrence = %s + WHERE bank_id = %s AND observation_id = %s AND topic_id = %s + AND observation_version = %s """, ( new_occ, @@ -521,7 +478,7 @@ def save_episode(self, episode: Episode) -> None: ), ) - # 6. Process DELETE mutations (items not in current context) + # 6. Process DELETE mutations (observations not in current context) for mutation in episode.mutations: if mutation.type == OpType.DELETE and mutation.item_id: if mutation.item_id not in processed_item_ids: @@ -529,7 +486,7 @@ def save_episode(self, episode: Episode) -> None: cur.execute( """ SELECT COALESCE(MAX(version), 0) + 1 as next_ver - FROM items + FROM observations WHERE bank_id = %s AND id = %s """, (self.bank_id, mutation.item_id), @@ -539,8 +496,8 @@ def save_episode(self, episode: Episode) -> None: cur.execute( """ - INSERT INTO items - (bank_id, id, version, section, content, episode_id, step, op_type, previous_content, ordinal) + INSERT INTO observations + (bank_id, id, version, type, content, episode_id, step, op_type, previous_content, ordinal) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( @@ -556,7 +513,7 @@ def save_episode(self, episode: Episode) -> None: 0, ), ) - # Note: DELETE items are NOT inserted into episode_items + # Note: DELETE observations are NOT inserted into episode_observations # 7. Insert artifacts for name, content in (episode.artifacts or {}).items(): @@ -572,7 +529,7 @@ def save_episode(self, episode: Episode) -> None: conn.commit() logger.info( - "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, items=%d)", + "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d)", episode.id, self.bank_id, episode.task, len(episode.context_memory.topics), len(episode.context_memory.all_items()), @@ -594,7 +551,7 @@ def load_context( matches 'owner/repo', 'owner/repo/pkg', etc.). Mutually exclusive with topic_ids. - Returns ContextMemory (topics + items + bindings) or None if + Returns ContextMemory (topics + observations + bindings) or None if no prior episode exists. No mutations, artifacts, or episode metadata are loaded. """ @@ -651,60 +608,60 @@ def load_context( # 2. Load topics from that episode cur.execute( """ - SELECT t.id, t.description FROM episode_topics et + SELECT t.id, t.type, t.description FROM episode_topics et JOIN topics t ON t.bank_id = et.bank_id AND t.id = et.topic_id WHERE et.bank_id = %s AND et.episode_id = %s """, (self.bank_id, episode_id), ) topics = [ - Topic(id=row["id"], description=row["description"]) + Topic(id=row["id"], type=row["type"], description=row["description"]) for row in cur.fetchall() ] - # 3. Load items at their LATEST version (not the pinned version) + # 3. Load observations at their LATEST version (not the pinned version) cur.execute( """ - SELECT DISTINCT ON (i.id) i.id, i.section, i.content, i.version - FROM episode_items ei - JOIN items i ON i.bank_id = ei.bank_id AND i.id = ei.item_id - WHERE ei.bank_id = %s AND ei.episode_id = %s - AND i.op_type != 'DELETE' - ORDER BY i.id, i.version DESC + SELECT DISTINCT ON (o.id) o.id, o.type, o.content, o.version + FROM episode_observations eo + JOIN observations o ON o.bank_id = eo.bank_id AND o.id = eo.observation_id + WHERE eo.bank_id = %s AND eo.episode_id = %s + AND o.op_type != 'DELETE' + ORDER BY o.id, o.version DESC """, (self.bank_id, episode_id), ) items_by_id: dict[str, dict] = {} for row in cur.fetchall(): items_by_id[row["id"]] = { - "section": row["section"], + "section": row["type"], "content": row["content"], "version": row["version"], } - # 4. Load item-topic bindings for those latest versions + # 4. Load observation-topic bindings for those latest versions if items_by_id: cur.execute( """ - SELECT it.item_id, it.topic_id, it.item_occurrence, it.version_occurrence - FROM item_topics it - WHERE it.bank_id = %s - AND (it.item_id, it.item_version) IN ( - SELECT i.id, MAX(i.version) - FROM episode_items ei - JOIN items i ON i.bank_id = ei.bank_id AND i.id = ei.item_id - WHERE ei.bank_id = %s AND ei.episode_id = %s AND i.op_type != 'DELETE' - GROUP BY i.id + SELECT ot.observation_id, ot.topic_id, ot.observation_occurrence, ot.version_occurrence + FROM observation_topics ot + WHERE ot.bank_id = %s + AND (ot.observation_id, ot.observation_version) IN ( + SELECT o.id, MAX(o.version) + FROM episode_observations eo + JOIN observations o ON o.bank_id = eo.bank_id AND o.id = eo.observation_id + WHERE eo.bank_id = %s AND eo.episode_id = %s AND o.op_type != 'DELETE' + GROUP BY o.id ) """, (self.bank_id, self.bank_id, episode_id), ) item_topics: dict[str, list[str]] = {} for row in cur.fetchall(): - item_id = row["item_id"] - if item_id not in item_topics: - item_topics[item_id] = [] - item_topics[item_id].append(row["topic_id"]) + observation_id = row["observation_id"] + if observation_id not in item_topics: + item_topics[observation_id] = [] + item_topics[observation_id].append(row["topic_id"]) # Build ContextMemory sections sections: dict[str, list[Item]] = { @@ -734,12 +691,12 @@ def load_context( setattr(ctx, section_name, items) total_items = sum(len(items) for items in sections.values()) - logger.debug("load_context: loaded %d topics, %d items from episode %s", len(topics), total_items, episode_id) + logger.debug("load_context: loaded %d topics, %d observations from episode %s", len(topics), total_items, episode_id) return ctx - # No items - return empty ContextMemory with topics - logger.debug("load_context: loaded %d topics, 0 items from episode %s", len(topics), episode_id) + # No observations - return empty ContextMemory with topics + logger.debug("load_context: loaded %d topics, 0 observations from episode %s", len(topics), episode_id) return ContextMemory(topics=topics) except Exception: logger.warning("load_context failed (bank=%s, task=%s)", self.bank_id, task, exc_info=True) diff --git a/src/codespy/agents/reviewer/models.py b/src/codespy/agents/reviewer/models.py index 69b1018..dbc34c5 100644 --- a/src/codespy/agents/reviewer/models.py +++ b/src/codespy/agents/reviewer/models.py @@ -46,11 +46,12 @@ def to_topic(self) -> "Topic": """Build a Topic representing this PR. Returns: - Topic object with id as PR URL and description as "PR #N: Title" + Topic object with id as PR URL, type as "pull_request", and description as "PR #N: Title" """ from codespy.agents.memory.hippocampus.context_memory import Topic return Topic( id=self.pr_url, + type="pull_request", description=f"PR #{self.pr_number}: {self.pr_title}"[:500], ) @@ -178,7 +179,7 @@ def topic(self, repo_full_name: str) -> 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) + return Topic(id=topic_id, type="project_scope", description=self.description) class Issue(BaseModel): diff --git a/src/codespy/agents/reviewer/modules/auditor.py b/src/codespy/agents/reviewer/modules/auditor.py index 9089685..0d7adf7 100644 --- a/src/codespy/agents/reviewer/modules/auditor.py +++ b/src/codespy/agents/reviewer/modules/auditor.py @@ -92,10 +92,7 @@ def _call_auditor( for scope in scopes or []: scope_topic = scope.topic(review_context.pr_context.repo_full_name) if scope_topic: - scope_topics.append(Topic( - id=scope_topic.id, - description=scope_topic.description, - )) + scope_topics.append(scope_topic) mem = Hippocampus( auditor, diff --git a/src/codespy/agents/reviewer/modules/scope_resolver.py b/src/codespy/agents/reviewer/modules/scope_resolver.py index feabb76..c307d00 100644 --- a/src/codespy/agents/reviewer/modules/scope_resolver.py +++ b/src/codespy/agents/reviewer/modules/scope_resolver.py @@ -1024,7 +1024,6 @@ async def _refine_scopes( ContextMemory, Topic, compute_common_ancestor_topic_id, - make_topic_id, ) # Local bindings from review_context metadata @@ -1120,22 +1119,13 @@ async def _refine_scopes( if scope.subroot in boundary_descriptions: scope.description = boundary_descriptions[scope.subroot] - # Build topic IDs - 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(pr.repo_full_name, scope.subroot, pkg_name) - scope_topic_ids[scope.subroot] = tid - # Build Topics scope_topics: list[Topic] = [] + scope_topic_ids: dict[str, str] = {} for scope in final_scopes: - scope_topics.append( - Topic( - id=scope_topic_ids[scope.subroot], - description=scope.description, - ) - ) + t = scope.topic(pr.repo_full_name) + scope_topics.append(t) + scope_topic_ids[scope.subroot] = t.id # Compute common ancestor topic if >1 scope common_ancestor_topic_id = compute_common_ancestor_topic_id( @@ -1145,7 +1135,7 @@ async def _refine_scopes( # 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)) + scope_topics.append(Topic(id=common_ancestor_topic_id, type="project_scope", description=common_desc)) stamp_topic_ids = [common_ancestor_topic_id] elif scope_topics: # Single scope: stamp with its topic ID @@ -1154,10 +1144,7 @@ async def _refine_scopes( stamp_topic_ids = [] # Add PR URL topic (provides description; not a scope) pr_ctx = review_context.pr_context - scope_topics.append(Topic( - id=pr_ctx.pr_url, - description=f"PR #{pr_ctx.pr_number}: {pr_ctx.pr_title}"[:500], - )) + scope_topics.append(pr_ctx.to_topic()) # Attach hierarchical skills to each produced scope for scope in final_scopes: scope.skills = collect_skills(repo_path, scope.subroot) diff --git a/src/codespy/agents/reviewer/modules/summarizer.py b/src/codespy/agents/reviewer/modules/summarizer.py index 1b48b8e..ebe8a13 100644 --- a/src/codespy/agents/reviewer/modules/summarizer.py +++ b/src/codespy/agents/reviewer/modules/summarizer.py @@ -114,10 +114,7 @@ def forward( for scope in scopes or []: scope_topic = scope.topic(pr_context.repo_full_name) if scope_topic: - scope_topics.append(Topic( - id=scope_topic.id, - description=scope_topic.description, - )) + scope_topics.append(scope_topic) mem = Hippocampus( summarizer, diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index d945cbc..50fed0b 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -229,7 +229,7 @@ class TestContextMemoryApply: def test_add_operation_with_topic_ids(self): """ADD operation assigns topic_ids to new items.""" - memory = ContextMemory(topics=[Topic(id="t1", description="Test")]) + memory = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="Test")]) ops = [Operation(type=OpType.ADD, section="context_understanding", content="New item")] new_memory, new_ids = memory.apply(ops, topic_ids=["t1"]) @@ -338,16 +338,16 @@ class TestContextMemoryMerge: 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")]) + mem1 = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="First")]) + mem2 = ContextMemory(topics=[Topic(id="t1", type="project_scope", 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")]) + mem1 = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="")]) + mem2 = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="Better description")]) merged = ContextMemory.merge(mem1, mem2) assert merged.topics[0].description == "Better description" @@ -368,15 +368,15 @@ def test_merge_items_by_id(self): def test_merge_multiple_memories(self): """Merge can handle multiple memories.""" mem1 = ContextMemory( - topics=[Topic(id="t1", description="T1")], + topics=[Topic(id="t1", type="project_scope", description="T1")], context_understanding=[Item(id="cu-1", content="Item 1", topic_ids=["t1"])], ) mem2 = ContextMemory( - topics=[Topic(id="t2", description="T2")], + topics=[Topic(id="t2", type="project_scope", description="T2")], context_understanding=[Item(id="cu-2", content="Item 2", topic_ids=["t2"])], ) mem3 = ContextMemory( - topics=[Topic(id="t3", description="T3")], + topics=[Topic(id="t3", type="project_scope", description="T3")], domain_constants=[Item(id="dc-1", content="Constant", topic_ids=["t3"])], ) merged = ContextMemory.merge(mem1, mem2, mem3) diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py index 29d8486..33b04fa 100644 --- a/tests/test_hippocampus.py +++ b/tests/test_hippocampus.py @@ -68,7 +68,7 @@ def test_save_simple_episode(self, episode_store): """Should save a simple episode with context memory.""" # Create a simple episode ctx = ContextMemory( - topics=[Topic(id="test/topic", description="Test topic")], + topics=[Topic(id="test/topic", type="project_scope", description="Test topic")], context_understanding=[ Item(id="cu-1", content="Test item", topic_ids=["test/topic"]) ], @@ -102,7 +102,7 @@ def test_save_and_load_episode_roundtrip(self, episode_store): # Create and save an episode ctx = ContextMemory( - topics=[Topic(id="owner/repo/pkg", description="Test package")], + topics=[Topic(id="owner/repo/pkg", type="project_scope", description="Test package")], context_understanding=[ Item(id="cu-abc123", content="Test understanding", topic_ids=["owner/repo/pkg"]) ], @@ -145,7 +145,7 @@ def test_item_versioning_on_replace(self, episode_store): # Episode 1: Add an item ctx1 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id="cu-item1", content="Original content", topic_ids=[topic_id]) ], @@ -175,7 +175,7 @@ def test_item_versioning_on_replace(self, episode_store): # Episode 2: Replace the item ctx2 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id="cu-item1", content="Updated content", topic_ids=[topic_id]) ], @@ -221,7 +221,7 @@ def test_inherited_items_preserved(self, episode_store): # Episode 1: Add two items ctx1 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id="cu-item1", content="Item 1 content", topic_ids=[topic_id]), Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), @@ -261,7 +261,7 @@ def test_inherited_items_preserved(self, episode_store): # Episode 2: Only replace item1, item2 is inherited ctx2 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id="cu-item1", content="Item 1 updated", topic_ids=[topic_id]), Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), # inherited @@ -320,7 +320,7 @@ def forward(self, **kwargs): MockAgent(), task_name="test_task", run_id="test-run", - topics=[Topic(id="test/topic", description="Test topic")], + topics=[Topic(id="test/topic", type="project_scope", description="Test topic")], ) # Make a call @@ -343,7 +343,7 @@ def test_load_context_returns_latest_episode(self, episode_store): # Save multiple episodes for i in range(3): ctx = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id=f"cu-item{i}", content=f"Content {i}", topic_ids=[topic_id]) ], @@ -383,7 +383,7 @@ def test_delete_creates_tombstone(self, episode_store): # Episode 1: Add an item ctx1 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ Item(id="cu-item1", content="To be deleted", topic_ids=[topic_id]) ], @@ -413,7 +413,7 @@ def test_delete_creates_tombstone(self, episode_store): # Episode 2: Delete the item (item not in context_memory, but in mutations) ctx2 = ContextMemory( - topics=[Topic(id=topic_id, description="Test repo")], + topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[], # Empty after delete ) episode2 = Episode( From 305ada4f9edf053c9d7d17a5746e0afa98b6cd7e Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 10 Sep 2026 10:38:35 +0200 Subject: [PATCH 07/14] doc --- docs/memory.md | 185 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 169 insertions(+), 16 deletions(-) diff --git a/docs/memory.md b/docs/memory.md index a3f2d1c..3dd4314 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -10,6 +10,15 @@ parsing schemas — and reuse it in subsequent reviews of the same code area. ## Concepts +### Banks + +A bank is the top-level data partition. All episodes, topics, observations, +and artifacts cascade-delete from a bank. + +- Configured via `MEMORY_BANK_ID` (default: `codespy`) +- Typical values: service name, team name, org identifier +- Changing the bank ID starts a fresh memory silo — no data carries over + ### Topics - Every scope gets a `topic_id` derived from `make_topic_id(repo_slug, subroot)` @@ -18,23 +27,154 @@ parsing schemas — and reuse it in subsequent reviews of the same code area. ### Episodes -- An Episode captures one agent's run: task, context_memory, mutations, timestamp +- An Episode captures one agent's run: task, context_memory, mutations, artifacts, timestamp - Stored in PostgreSQL (auto-created tables). pg0-embedded auto-starts a local instance when no `MEMORY_POSTGRES_URI` is set. -- `find_latest_episode()` loads the most recent episode by `modified_at` for a given path prefix +- `EpisodeStore.load_context(task, topic_ids, topic_prefix)` retrieves + the latest context by `timestamp DESC`, filtering on bank + task + topic ### Context Memory Six 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. **`actions`** — Tool execution patterns and action history -5. **`parsing_schema`** — File format conventions, naming patterns, structural rules -6. **`reusable_results`** — Computed facts reusable in future reviews +1. **`context_roadmap`** — Index of what the context contains and where to find it +2. **`context_understanding`** — High-level understanding of the context +3. **`domain_constants`** — Exact parameters, formulas, thresholds, reference values, enum sets +4. **`actions`** — Tool execution patterns: what tool was used, for what purpose, and the result +5. **`parsing_schema`** — How to parse the context's format: delimiters, boundary patterns, field structure +6. **`reusable_results`** — Agent-derived aggregated outputs (counts, distributions, classifications) reusable across questions + +Each section contains `Item` objects with `id`, `content`, and `topic_ids` linking +the item to its relevant scopes. + +### Observations + +Observations are the versioned audit trail of context memory items in the database. +Each time the Cartographer ADDs, REPLACEs, or DELETEs an item, a new observation +row is inserted with an incremented `version` number. + +- `content` holds the new value (`NULL` for DELETE) +- `previous_content` preserves the prior state (for REPLACE / DELETE) +- `op_type` records the operation: `ADD`, `REPLACE`, or `DELETE` +- Linked to the episode that produced the mutation and the topics it belongs to + +Items in `ContextMemory` map 1:1 to the latest non-deleted observation version. + +### Artifacts + +Named text outputs attached to an episode — for example, the final review +markdown or the PR summary text. Stored as `(episode_id, name) → content`. + +## Database Schema + +Auto-created by `EpisodeStore.ensure_schema()` on first connect. +Source: `src/codespy/agents/memory/postgres.py:61-211`. + +### Entity Relationship Diagram + +```mermaid +erDiagram + schema_version { + int version PK + timestamptz applied_at "DEFAULT now()" + } + + banks { + varchar(64) id PK + text description + } + + topics { + varchar(64) bank_id PK, FK + varchar(256) id PK + varchar(64) type + text description + } + + episodes { + varchar(64) bank_id PK, FK + uuid id PK + varchar(48) run_id + varchar(64) task + varchar(64) module + text question "DEFAULT ''" + timestamptz timestamp + } + + episode_topics { + varchar(64) bank_id PK, FK + uuid episode_id PK, FK + varchar(256) topic_id PK, FK + } + + observations { + varchar(64) bank_id PK, FK + varchar(48) id PK + int version PK "DEFAULT 1" + varchar(32) type + text content + uuid episode_id FK + int step "DEFAULT 0" + varchar(8) op_type "ADD | REPLACE | DELETE" + text previous_content + int ordinal "DEFAULT 0" + } + + observation_topics { + varchar(64) bank_id PK, FK + varchar(48) observation_id PK, FK + int observation_version PK, FK + varchar(256) topic_id PK, FK + int observation_occurrence "DEFAULT 0" + int version_occurrence "DEFAULT 0" + } + + episode_observations { + varchar(64) bank_id PK, FK + uuid episode_id PK, FK + varchar(48) observation_id PK, FK + int observation_version FK + } + + artifacts { + varchar(64) bank_id PK, FK + uuid episode_id PK, FK + text name PK + text content + } + + banks ||--o{ topics : "has" + banks ||--o{ episodes : "has" + banks ||--o{ observations : "has" + episodes ||--o{ episode_topics : "tagged with" + topics ||--o{ episode_topics : "tags" + episodes ||--o{ observations : "creates" + episodes ||--o{ artifacts : "produces" + episodes ||--o{ episode_observations : "references" + observations ||--o{ episode_observations : "referenced by" + observations ||--o{ observation_topics : "scoped to" + topics ||--o{ observation_topics : "scopes" +``` + +### Indexes -Each section contains Items with tags (general, scope-specific) and text content. +| Index | Table | Definition | +|-------|-------|------------| +| `idx_topics_id_trgm` | topics | `GIN (id gin_trgm_ops)` — trigram fuzzy search | +| `idx_episodes_task_time` | episodes | `(bank_id, task, timestamp DESC)` | +| `idx_episode_topics_reverse` | episode_topics | `(bank_id, topic_id)` | +| `idx_observations_episode` | observations | `(bank_id, episode_id)` | +| `idx_observation_topics_reverse` | observation_topics | `(bank_id, topic_id)` | +| `idx_episode_observations_reverse` | episode_observations | `(bank_id, observation_id)` | + +### Notes + +- All tables cascade-delete from `banks` +- `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE history per item +- `episode_topics` and `observation_topics` are M:N junction tables +- `episode_observations` links episodes to the specific observation versions they reference +- `observation_topics.observation_occurrence` counts cumulative topic associations across all versions; `version_occurrence` counts within one version +- Requires PostgreSQL extension: `pg_trgm` ## Reflection Pipeline @@ -47,16 +187,25 @@ After each agent run (at `end_episode()`): - `DELETE` — Remove outdated/irrelevant item 3. **Eviction** — If memory exceeds `max_context_memory_tokens`, oldest general items are evicted first +The Distiller also tags each existing context memory item with an `ItemTag`: + +- **`helpful`** — directly aided the agent; keep +- **`harmful`** — misled the agent or contradicted observations; remove +- **`neutral`** — present but unused this round; keep +- **`stale`** — no longer reflects the external context; remove + +These tags inform the Cartographer's edit decisions. + 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` | 16384 | Ceiling on persisted ContextMemory (re-sent every iteration) | -| Item | `MEMORY_DEFAULT_MAX_CONTEXT_ITEM_TOKENS` | 512 | Soft per-item limit (expressed to LLM, not truncated) | -| Trajectory | `MEMORY_DEFAULT_MAX_TRAJECTORY_TOKENS` | 16384 | Head+tail cap on trajectory fed to Distiller | -| Question | `MEMORY_DEFAULT_MAX_QUESTION_TOKENS` | 8192 | Cap on serialized inputs as reflection question | +| Context memory | `MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | 16384 | Ceiling on persisted ContextMemory (re-sent every iteration) | +| Item | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | 512 | Soft per-item limit (expressed to LLM, not truncated) | +| Trajectory | `MEMORY_MAX_TRAJECTORY_TOKENS` | 16384 | Head+tail cap on trajectory fed to Distiller | +| Question | `MEMORY_MAX_QUESTION_TOKENS` | 8192 | Cap on serialized inputs as reflection question | Item capacity ≈ context_memory_tokens / item_tokens (16384/512 = 32 items) @@ -70,8 +219,10 @@ Item capacity ≈ context_memory_tokens / item_tokens (16384/512 = 32 items) | `MEMORY_BANK_ID` | `memory.bank_id` | `codespy` | Scopes all memory data | | `MEMORY_PG0_NAME` | `memory.pg0_name` | `codespy` | pg0-embedded database name | | `MEMORY_PG0_PORT` | `memory.pg0_port` | auto | pg0-embedded port | +| `MEMORY_PG0_DATA_DIR` | `memory.pg0_data_dir` | — | Custom data directory for pg0-embedded | | `MEMORY_DEFAULT_ENABLED` | `memory.default_enabled` | `false` | Enable memory globally | | `MEMORY_DEFAULT_MAX_REFLECTS` | `memory.default_max_reflects` | `0` | Reflection iterations | +| `MEMORY_COMPACT_TRAJECTORY` | `memory.compact_trajectory` | `true` | Apply head+tail trajectory bounding before distillation | ### Reflection Module LLM Overrides @@ -109,9 +260,11 @@ MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ``` -# Storage: pg0-embedded auto-starts when pg0-embedded is installed (default). -# For production, set MEMORY_POSTGRES_URI: -# MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy +> **Note:** pg0-embedded auto-starts when installed (default for local dev). +> For production, set `MEMORY_POSTGRES_URI`: +> ``` +> MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy +> ``` ### GitHub Action From c6be89f52621ffdb0bf23a565e8e9dc78779c655 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 10 Sep 2026 11:53:08 +0200 Subject: [PATCH 08/14] doc --- docs/configuration.md | 2 +- docs/memory.md | 28 +-- .../agents/memory/hippocampus/__init__.py | 8 +- .../memory/hippocampus/context_memory.py | 169 ++++++------------ .../agents/memory/hippocampus/hippocampus.py | 56 +++--- .../hippocampus/modules/cartographer.py | 54 +++--- .../memory/hippocampus/modules/distiller.py | 34 ++-- src/codespy/agents/memory/postgres.py | 104 +++++------ tests/test_context_memory.py | 121 ++++--------- tests/test_hippocampus.py | 125 +++++++------ 10 files changed, 294 insertions(+), 407 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 79f47ed..f0f1eed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -179,7 +179,7 @@ Brief overview: | 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_MAX_CONTEXT_MEMORY_TOKENS` | `16384` | Ceiling on persisted context memory | -| Item tokens | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | `512` | Soft per-item token limit | +| Observation tokens | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | `512` | Soft per-observation token limit | | Trajectory tokens | `MEMORY_MAX_TRAJECTORY_TOKENS` | `16384` | Cap on trajectory fed to Distiller | | Question tokens | `MEMORY_MAX_QUESTION_TOKENS` | `8192` | Cap on serialized reflection inputs | diff --git a/docs/memory.md b/docs/memory.md index 3dd4314..d41ebf8 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -44,21 +44,21 @@ Six sections (from general to specific): 5. **`parsing_schema`** — How to parse the context's format: delimiters, boundary patterns, field structure 6. **`reusable_results`** — Agent-derived aggregated outputs (counts, distributions, classifications) reusable across questions -Each section contains `Item` objects with `id`, `content`, and `topic_ids` linking -the item to its relevant scopes. +Each section contains `Observation` objects with `id`, `content`, and `topic_ids` linking +the observation to its relevant scopes. ### Observations Observations are the versioned audit trail of context memory items in the database. -Each time the Cartographer ADDs, REPLACEs, or DELETEs an item, a new observation -row is inserted with an incremented `version` number. +Each time the Cartographer ADDs, REPLACEs, or DELETEs an observation, a new +observation row is inserted with an incremented `version` number. - `content` holds the new value (`NULL` for DELETE) - `previous_content` preserves the prior state (for REPLACE / DELETE) - `op_type` records the operation: `ADD`, `REPLACE`, or `DELETE` - Linked to the episode that produced the mutation and the topics it belongs to -Items in `ContextMemory` map 1:1 to the latest non-deleted observation version. +Observations in `ContextMemory` map 1:1 to the latest non-deleted observation version. ### Artifacts @@ -170,7 +170,7 @@ erDiagram ### Notes - All tables cascade-delete from `banks` -- `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE history per item +- `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE history per observation - `episode_topics` and `observation_topics` are M:N junction tables - `episode_observations` links episodes to the specific observation versions they reference - `observation_topics.observation_occurrence` counts cumulative topic associations across all versions; `version_occurrence` counts within one version @@ -180,14 +180,14 @@ erDiagram 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 +1. **Distiller** — Analyzes the agent's trajectory (head 60% + tail 40%, capped at `max_trajectory_tokens`) and proposes `CacheCandidate` observations 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 + - `ADD` — Insert new observation + - `REPLACE` — Update existing observation with new knowledge + - `DELETE` — Remove outdated/irrelevant observation +3. **Eviction** — If memory exceeds `max_context_memory_tokens`, oldest general observations are evicted first -The Distiller also tags each existing context memory item with an `ItemTag`: +The Distiller also tags each existing context memory observation with an `ObservationTag`: - **`helpful`** — directly aided the agent; keep - **`harmful`** — misled the agent or contradicted observations; remove @@ -203,11 +203,11 @@ Reflection iterates `max_reflects` times (0 = reflect once at end_episode). | Budget | Env Var | Default | Purpose | |--------|---------|---------|---------| | Context memory | `MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | 16384 | Ceiling on persisted ContextMemory (re-sent every iteration) | -| Item | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | 512 | Soft per-item limit (expressed to LLM, not truncated) | +| Observation | `MEMORY_MAX_CONTEXT_ITEM_TOKENS` | 512 | Soft per-observation limit (expressed to LLM, not truncated) | | Trajectory | `MEMORY_MAX_TRAJECTORY_TOKENS` | 16384 | Head+tail cap on trajectory fed to Distiller | | Question | `MEMORY_MAX_QUESTION_TOKENS` | 8192 | Cap on serialized inputs as reflection question | -Item capacity ≈ context_memory_tokens / item_tokens (16384/512 = 32 items) +Observation capacity ≈ context_memory_tokens / observation_tokens (16384/512 = 32 observations) ## Configuration diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index 7fd017c..421c093 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -2,9 +2,9 @@ from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, ContextMemory, - Item, - ItemTag, Mutation, + Observation, + ObservationTag, Operation, OpType, SectionName, @@ -30,10 +30,10 @@ "DistillerSig", "Episode", "Hippocampus", - "Item", - "ItemTag", "MemoryBudget", "Mutation", + "Observation", + "ObservationTag", "Operation", "OpType", "SectionName", diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 72b1585..91dbaed 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -11,8 +11,8 @@ logger = logging.getLogger(__name__) -class ItemTag(StrEnum): - """How a context-memory item performed in the trajectory just observed. +class ObservationTag(StrEnum): + """How a context-memory observation performed in the trajectory just observed. - helpful: directly aided orientation or answering; keep. - harmful: misled the agent or contradicted observations; remove. @@ -26,7 +26,7 @@ class ItemTag(StrEnum): STALE = "stale" @classmethod - def _missing_(cls, value: object) -> ItemTag | None: + def _missing_(cls, value: object) -> ObservationTag | None: if isinstance(value, str): lower = value.lower() for member in cls: @@ -88,23 +88,23 @@ class Topic(BaseModel): description: str = Field(description="Human-readable description of this topic") -class Item(BaseModel): - """A single item in the context memory.""" +class Observation(BaseModel): + """A single observation in the context memory.""" - id: str = Field(description="Unique item identifier") - content: str = Field(description="Item content") + id: str = Field(description="Unique observation identifier") + content: str = Field(description="Observation content") topic_ids: list[str] = Field( - default_factory=list, description="IDs of topics this item belongs to" + default_factory=list, description="IDs of topics this observation belongs to" ) def bind_topics(self, topic_ids: list[str]) -> None: - """Bind this item to the given topic_ids (only if currently unbound).""" + """Bind this observation 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.""" + """A candidate observation to be added to the context memory.""" section: SectionName = Field( default="context_understanding", @@ -114,7 +114,7 @@ class CacheCandidate(BaseModel): ), ) value: str = Field( - description="Compact candidate cache item, within the max_context_item_tokens budget." + description="Compact candidate cache observation, within the max_context_item_tokens budget." ) transferability: str = Field( default="", @@ -134,7 +134,7 @@ class Operation(BaseModel): validation_alias=AliasChoices("type", "op"), ) section: SectionName | None = Field(default=None, description="Required for ADD.") - item_id: str | None = Field(default=None, description="Required for DELETE / REPLACE.") + observation_id: str | None = Field(default=None, description="Required for DELETE / REPLACE.") content: str | None = Field(default=None, description="Required for ADD / REPLACE.") @@ -147,8 +147,8 @@ class Mutation(BaseModel): 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") + observation_id: str = Field(description="Generated ID (ADD) or existing ID (DELETE/REPLACE)") + section: SectionName = Field(description="Section the observation belongs to") content: str | None = Field( default=None, description="New content (ADD/REPLACE); None for DELETE" ) @@ -161,42 +161,42 @@ class Mutation(BaseModel): class ContextMemory(BaseModel): - """Context memory with topics and sectioned items. + """Context memory with topics and sectioned observations. - Topic-aware structure where each Item links to one or more topics via topic_ids. + Topic-aware structure where each Observation 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( + context_roadmap: list[Observation] = Field( default_factory=list, description="Index of what the context contains and where to find it", ) - context_understanding: list[Item] = Field( + context_understanding: list[Observation] = Field( default_factory=list, description="High-level understanding of the context", ) - domain_constants: list[Item] = Field( + domain_constants: list[Observation] = Field( default_factory=list, description=( "Exact parameters, formulas, thresholds, reference values, " "enum sets, and output field requirements" ), ) - parsing_schema: list[Item] = Field( + parsing_schema: list[Observation] = Field( default_factory=list, description=( "How to parse and navigate the context's format: " "delimiters, boundary patterns, field structure" ), ) - reusable_results: list[Item] = Field( + reusable_results: list[Observation] = Field( default_factory=list, description=( "Agent-derived aggregated outputs (counts, distributions, classifications) " "that multiple questions would need" ), ) - actions: list[Item] = Field( + actions: list[Observation] = Field( default_factory=list, description=( "Tool execution action patterns: what tool was used, for what purpose, " @@ -209,44 +209,44 @@ 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.""" + def section(self, name: str) -> list[Observation]: + """Get observations 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. + """Set topics and bind all untagged observations 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. + default_topic_ids: topic_ids to assign to any observation 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) + for obs in self.section(sec): + obs.bind_topics(default_topic_ids) - def all_items(self) -> list[Item]: - """Return all items across all sections.""" + def all_observations(self) -> list[Observation]: + """Return all observations 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. + def find_observation(self, observation_id: str) -> tuple[SectionName, Observation] | None: + """Look up an observation by ID across all sections. Returns: - Tuple of (section_name, item) if found, None otherwise. + Tuple of (section_name, observation) 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] + for obs in self.section(sec): + if obs.id == observation_id: + return sec, obs # 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()} + """Return set of all observation IDs.""" + return {obs.id for obs in self.all_observations()} def apply( self, ops: list[Operation], topic_ids: list[str] | None = None @@ -255,124 +255,69 @@ def apply( Args: ops: List of operations to apply (ADD, DELETE, REPLACE) - topic_ids: Optional list of topic IDs to assign to new items + topic_ids: Optional list of topic IDs to assign to new observations Returns: - Tuple of (new ContextMemory, list of IDs of newly-added items) + Tuple of (new ContextMemory, list of IDs of newly-added observations) """ cm = self.model_copy(deep=True) new_ids: list[str] = [] for op in ops: - if op.type == OpType.DELETE and op.item_id: + if op.type == OpType.DELETE and op.observation_id: for sec in cm.section_names(): lst = cm.section(sec) - lst[:] = [it for it in lst if it.id != op.item_id] + lst[:] = [obs for obs in lst if obs.id != op.observation_id] - elif op.type == OpType.REPLACE and op.item_id and op.content: + elif op.type == OpType.REPLACE and op.observation_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: + for i, obs in enumerate(lst): + if obs.id == op.observation_id: # Preserve existing topic_ids on REPLACE - lst[i] = Item(id=it.id, content=op.content, topic_ids=it.topic_ids) + lst[i] = Observation(id=obs.id, content=op.content, topic_ids=obs.topic_ids) replaced = True break if replaced: break if not replaced: - # Infer section from item_id prefix; fall back to ADD - prefix = op.item_id.split("-", 1)[0] if "-" in op.item_id else "" + # Infer section from observation_id prefix; fall back to ADD + prefix = op.observation_id.split("-", 1)[0] if "-" in op.observation_id else "" section_name = _PREFIX_TO_SECTION.get(prefix) if section_name: logger.info( "REPLACE target %r not found; falling back to ADD in %s", - op.item_id, section_name, + op.observation_id, section_name, ) new_id = f"{prefix}-{uuid.uuid4().hex}" - new_item = Item(id=new_id, content=op.content, topic_ids=topic_ids or []) - cm.section(section_name).append(new_item) + new_obs = Observation(id=new_id, content=op.content, topic_ids=topic_ids or []) + cm.section(section_name).append(new_obs) new_ids.append(new_id) else: logger.warning( - "REPLACE item_id %r has no valid prefix — " + "REPLACE observation_id %r has no valid prefix — " "likely a topic ID; skipping", - op.item_id, + op.observation_id, ) 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_obs = Observation(id=new_id, content=op.content, topic_ids=topic_ids or []) + cm.section(op.section).append(new_obs) new_ids.append(new_id) return cm, new_ids def without(self, ids: set[str]) -> ContextMemory: - """Return a new ContextMemory without the specified items.""" + """Return a new ContextMemory without the specified observations.""" 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] + lst[:] = [obs for obs in lst if obs.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. diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index cebc5c1..bd40ce9 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -21,7 +21,7 @@ ) from codespy.agents.memory.hippocampus.context_memory import ( ContextMemory, - ItemTag, + ObservationTag, Mutation, Operation, OpType, @@ -170,7 +170,7 @@ class for per-field guidance. Resolve one from configuration with provided, the agent starts with this memory instead of an empty one, inheriting accumulated understanding from upstream pipeline stages. topics: Optional list of Topic objects to register in the context memory. - Topic IDs are auto-assigned to all new items created during this episode. + Topic IDs are auto-assigned to all new observations created during this episode. Used for scope-aware memory organization. """ super().__init__() @@ -420,11 +420,11 @@ def _record_mutations( """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. + For ADD, back-fills observation_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. + new_ids: IDs of observations created by apply() in the same order as ADD ops. pre_memory: Context memory state before apply() — used to look up previous content for DELETE/REPLACE. @@ -434,46 +434,46 @@ def _record_mutations( mutations: list[Mutation] = [] 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) + if op.type == OpType.DELETE and op.observation_id: + found = pre_memory.find_observation(op.observation_id) if found: - section, old_item = found + section, old_obs = found mutations.append( Mutation( step=self._distill_step, type=OpType.DELETE, - item_id=op.item_id, + observation_id=op.observation_id, section=section, content=None, - previous_content=old_item.content, - topic_ids=old_item.topic_ids, + previous_content=old_obs.content, + topic_ids=old_obs.topic_ids, ) ) - elif op.type == OpType.REPLACE and op.item_id and op.content: - found = pre_memory.find_item(op.item_id) + elif op.type == OpType.REPLACE and op.observation_id and op.content: + found = pre_memory.find_observation(op.observation_id) if found: - section, old_item = found + section, old_obs = found mutations.append( Mutation( step=self._distill_step, type=OpType.REPLACE, - item_id=op.item_id, + observation_id=op.observation_id, section=section, content=op.content, - previous_content=old_item.content, - topic_ids=old_item.topic_ids, + previous_content=old_obs.content, + topic_ids=old_obs.topic_ids, ) ) else: # Fallback REPLACE→ADD: mirrors apply()'s fallback # so add_mutations stays aligned with new_ids - prefix = op.item_id.split("-", 1)[0] if "-" in op.item_id else "" + prefix = op.observation_id.split("-", 1)[0] if "-" in op.observation_id else "" section_name = _PREFIX_TO_SECTION.get(prefix) if section_name: mut = Mutation( step=self._distill_step, type=OpType.ADD, - item_id="", # back-filled from new_ids + observation_id="", # back-filled from new_ids section=section_name, content=op.content, previous_content=None, @@ -486,7 +486,7 @@ def _record_mutations( mut = Mutation( step=self._distill_step, type=OpType.ADD, - item_id="", + observation_id="", section=op.section, content=op.content, previous_content=None, @@ -494,20 +494,20 @@ def _record_mutations( ) mutations.append(mut) add_mutations.append(mut) - # Back-fill ADD mutation item_ids from new_ids + # Back-fill ADD mutation observation_ids from new_ids for mut, new_id in zip(add_mutations, new_ids, strict=True): - mut.item_id = new_id + mut.observation_id = new_id return mutations - def _update_item_scores(self, tags: dict[str, ItemTag]) -> None: - """Adjust item scores based on Distiller-assigned tags. + def _update_observation_scores(self, tags: dict[str, ObservationTag]) -> None: + """Adjust observation 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: + if tag == ObservationTag.HELPFUL: self.scores[bid] = self.scores.get(bid, 0) + 1 - elif tag in (ItemTag.HARMFUL, ItemTag.STALE): + elif tag in (ObservationTag.HARMFUL, ObservationTag.STALE): self.scores[bid] = self.scores.get(bid, 0) - 1 else: self.scores.setdefault(bid, 0) @@ -521,12 +521,12 @@ 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} - self._update_item_scores(tags) + tags = {k: v for k, v in (distilled.observation_tags or {}).items() if k in known} + self._update_observation_scores(tags) edits = self.cartograph( diagnosis=distilled.diagnosis, - item_tags=tags, + observation_tags=tags, cache_candidates=list(distilled.cache_candidates or []), current_map=self.cmem, question=question, diff --git a/src/codespy/agents/memory/hippocampus/modules/cartographer.py b/src/codespy/agents/memory/hippocampus/modules/cartographer.py index 023ed37..adf88e8 100644 --- a/src/codespy/agents/memory/hippocampus/modules/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/modules/cartographer.py @@ -6,7 +6,7 @@ from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, ContextMemory, - ItemTag, + ObservationTag, Operation, ) @@ -25,44 +25,44 @@ class CartographerSig(dspy.Signature): ## Instructions - Review the latest Distiller diagnosis and the current context memory. - - Prioritize items representing SHARED UNDERSTANDING — knowledge + - Prioritize observations 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. + - Keep observations that are structural, relational, or globally informative. + - Remove observations that are stale, misleading, redundant, or low-value. + - Rewrite observations 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 — stay within the + - Add new observations only when they represent transferable understanding. + - Each observation must be short and budget-efficient — stay within the `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. - The litmus test: For each item, ask "Would a future agent asking a + The litmus test: For each observation, 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. - ## How to use item_tags + ## How to use observation_tags - The Distiller assigns each existing item a tag. Let it drive your ops: - - harmful / stale → DELETE the item (unless a corrected REPLACE is + The Distiller assigns each existing observation a tag. Let it drive your ops: + - harmful / stale → DELETE the observation (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. + - neutral → keep as-is; do not churn ops on neutral observations. ## Operation rules Each operation has exactly these fields: - type: one of "ADD", "DELETE", or "REPLACE" - section: (ADD only) one of the six section names - - item_id: (DELETE/REPLACE only) existing item ID from current memory. - Item IDs have short prefixes (cu-, cr-, dc-, ps-, rr-, ac-). - NEVER use topic IDs (owner/repo paths or URLs) as item_id. + - observation_id: (DELETE/REPLACE only) existing observation ID from current memory. + Observation IDs have short prefixes (cu-, cr-, dc-, ps-, rr-, ac-). + NEVER use topic IDs (owner/repo paths or URLs) as observation_id. - content: (ADD/REPLACE only) the new content string - Only reference `item_id`s that exist in the current memory. Never invent - ids — new items get their ids assigned automatically on ADD. + Only reference `observation_id`s that exist in the current memory. Never invent + ids — new observations get their ids assigned automatically on ADD. ## Value Priority (highest to lowest) @@ -110,21 +110,21 @@ class CartographerSig(dspy.Signature): """ diagnosis: str = dspy.InputField(desc="Distiller's narrative diagnosis.") - item_tags: dict[str, ItemTag] = dspy.InputField( - desc="Per-item tags from the Distiller. Keys are item IDs (prefixed cu-, cr-, dc-, ps-, rr-, ac-), never topic IDs." + observation_tags: dict[str, ObservationTag] = dspy.InputField( + desc="Per-observation tags from the Distiller. Keys are observation IDs (prefixed cu-, cr-, dc-, ps-, rr-, ac-), never topic IDs." ) cache_candidates: list[CacheCandidate] = dspy.InputField( - desc="Candidate items the Distiller proposed." + desc="Candidate observations the Distiller proposed." ) current_map: ContextMemory = dspy.InputField( - desc="Current context memory. 'topics' are metadata (not editable items). " - "Editable items live in the six sections and have prefixed IDs (cu-, cr-, dc-, ps-, rr-, ac-)." + desc="Current context memory. 'topics' are metadata (not editable observations). " + "Editable observations live in the six sections and have prefixed IDs (cu-, cr-, dc-, ps-, rr-, ac-)." ) question: str = dspy.InputField(desc="Question the agent was answering.") 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 memory item. Every ADD/REPLACE content " + desc="Token budget for a SINGLE context memory observation. Every ADD/REPLACE content " "must stay within it." ) @@ -142,8 +142,8 @@ class Cartographer(dspy.Module): """Translates the Distiller's structured reflection into concrete edits 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 + Owns *what is worth keeping* — selects which tagged observations to drop, which + candidates to add, and which existing observations to rewrite. Token-budget enforcement is the Evictor's job. """ @@ -166,7 +166,7 @@ def __init__(self): def forward( self, diagnosis, - item_tags, + observation_tags, cache_candidates, current_map, question, @@ -182,7 +182,7 @@ def forward( with SignatureContext(self.SIGNATURE, get_cost_tracker()): return self.predict( diagnosis=diagnosis, - item_tags=item_tags, + observation_tags=observation_tags, cache_candidates=cache_candidates, current_map=current_map, question=question, diff --git a/src/codespy/agents/memory/hippocampus/modules/distiller.py b/src/codespy/agents/memory/hippocampus/modules/distiller.py index 5f37cad..ccdfd57 100644 --- a/src/codespy/agents/memory/hippocampus/modules/distiller.py +++ b/src/codespy/agents/memory/hippocampus/modules/distiller.py @@ -6,7 +6,7 @@ from codespy.agents.memory.hippocampus.context_memory import ( CacheCandidate, ContextMemory, - ItemTag, + ObservationTag, ) @@ -33,7 +33,7 @@ class DistillerSig(dspy.Signature): 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 + were asked about this same context, would this cached observation save the agent work?" ## Produce three outputs @@ -47,18 +47,18 @@ 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 context memory, tag it exactly: + 2. OBSERVATION_TAGS — For EVERY observation 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 - but plausibly useful for other questions + 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: + 3. CACHE_CANDIDATES — Observations to ADD. Value tiers: Highest value — structural understanding that transfers across questions: @@ -82,12 +82,12 @@ class DistillerSig(dspy.Signature): was. Focus on tool-use strategies that would save a future agent exploration work. Do NOT record every individual tool call — only patterns that a future run on the same context would benefit from. - - 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. + - Parsing schema: document delimiters, boundary patterns, field + format, how to reliably split or locate observations 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 @@ -125,7 +125,7 @@ class DistillerSig(dspy.Signature): ) 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 memory item. Keep every candidate within " + desc="Token budget for a SINGLE context memory observation. Keep every candidate within " "it; if one exceeds it, rewrite it more compactly or split it." ) @@ -134,12 +134,12 @@ class DistillerSig(dspy.Signature): "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 memory. " - "Keys must match existing item ids exactly." + observation_tags: dict[str, ObservationTag] = dspy.OutputField( + desc="Per-observation-id tag for EVERY observation currently in the context memory. " + "Keys must match existing observation ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( - desc="Candidate items to add. Each within the max_context_item_tokens budget; " + desc="Candidate observations to add. Each within the max_context_item_tokens budget; " "structural/transferable only. " "Each candidate's `section` must be one of the six section names above." ) @@ -151,7 +151,7 @@ class Distiller(dspy.Module): 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. + specific work, tags every existing observation, and proposes new candidates. """ # Name this module's settings live under: memory.distiller. diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index bdf9e67..36204d8 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -14,8 +14,8 @@ if TYPE_CHECKING: from codespy.agents.memory.hippocampus.context_memory import ( ContextMemory, - Item, Mutation, + Observation, Topic, ) from codespy.agents.memory.hippocampus.episode import Episode @@ -222,13 +222,13 @@ def save_episode(self, episode: Episode) -> None: Single transaction: 1. Ensure bank row exists (idempotent) 2. Upsert episode row (no-op if already exists) - 3. Upsert topics, insert items (versioned), insert junctions, insert artifacts + 3. Upsert topics, insert observations (versioned), insert junctions, insert artifacts """ # Import here to avoid circular imports from codespy.agents.memory.hippocampus.context_memory import ( ContextMemory, - Item, Mutation, + Observation, OpType, Topic, ) @@ -285,26 +285,26 @@ def save_episode(self, episode: Episode) -> None: ) # 5. Process observations and mutations - # Build map of item_id -> mutation for quick lookup + # Build map of observation_id -> mutation for quick lookup mutation_map: dict[str, Mutation] = {} for mut in episode.mutations: - mutation_map[mut.item_id] = mut + mutation_map[mut.observation_id] = mut # Get all current observations from context_memory - current_items: dict[str, tuple[str, Item]] = {} # observation_id -> (type, observation) + current_observations: dict[str, tuple[str, Observation]] = {} # observation_id -> (type, observation) for sec in episode.context_memory.section_names(): - for item in getattr(episode.context_memory, sec): - current_items[item.id] = (sec, item) + for obs in getattr(episode.context_memory, sec): + current_observations[obs.id] = (sec, obs) # Track observations we've processed to detect DELETEs - processed_item_ids: set[str] = set() + processed_observation_ids: set[str] = set() # Process observations in current context memory - for item_id, (type, item) in current_items.items(): - processed_item_ids.add(item_id) - mutation = mutation_map.get(item_id) + for observation_id, (type, obs) in current_observations.items(): + processed_observation_ids.add(observation_id) + mutation = mutation_map.get(observation_id) - # Determine if this is a new item (ADD) or existing (REPLACE/inherited) + # Determine if this is a new observation (ADD) or existing (REPLACE/inherited) is_new = False version = 1 op_type = "ADD" @@ -325,7 +325,7 @@ def save_episode(self, episode: Episode) -> None: FROM observations WHERE bank_id = %s AND id = %s """, - (self.bank_id, item_id), + (self.bank_id, observation_id), ) row = cur.fetchone() if row and row["max_ver"]: @@ -341,7 +341,7 @@ def save_episode(self, episode: Episode) -> None: FROM observations WHERE bank_id = %s AND id = %s """, - (self.bank_id, item_id), + (self.bank_id, observation_id), ) row = cur.fetchone() version = row["next_ver"] if row else 1 @@ -357,10 +357,10 @@ def save_episode(self, episode: Episode) -> None: """, ( self.bank_id, - item_id, + observation_id, version, type, - item.content, + obs.content, str(episode.id), step, op_type, @@ -376,11 +376,11 @@ def save_episode(self, episode: Episode) -> None: VALUES (%s, %s, %s, %s) ON CONFLICT (bank_id, episode_id, observation_id) DO NOTHING """, - (self.bank_id, str(episode.id), item_id, version), + (self.bank_id, str(episode.id), observation_id, version), ) # Insert observation_topics for this version - for topic_id in item.topic_ids: + for topic_id in obs.topic_ids: # Count occurrences cur.execute( """ @@ -390,7 +390,7 @@ def save_episode(self, episode: Episode) -> None: ORDER BY observation_version DESC LIMIT 1 """, - (self.bank_id, item_id, topic_id), + (self.bank_id, observation_id, topic_id), ) prev_row = cur.fetchone() if prev_row: @@ -413,7 +413,7 @@ def save_episode(self, episode: Episode) -> None: """, ( self.bank_id, - item_id, + observation_id, version, topic_id, observation_occ, @@ -428,7 +428,7 @@ def save_episode(self, episode: Episode) -> None: FROM observations WHERE bank_id = %s AND id = %s """, - (self.bank_id, item_id), + (self.bank_id, observation_id), ) row = cur.fetchone() existing_version = row["max_ver"] if row and row["max_ver"] else 1 @@ -443,13 +443,13 @@ def save_episode(self, episode: Episode) -> None: ( self.bank_id, str(episode.id), - item_id, + observation_id, existing_version, ), ) # Increment observation_occurrence for inherited observations - for topic_id in item.topic_ids: + for topic_id in obs.topic_ids: cur.execute( """ SELECT observation_occurrence @@ -457,7 +457,7 @@ def save_episode(self, episode: Episode) -> None: WHERE bank_id = %s AND observation_id = %s AND topic_id = %s AND observation_version = %s """, - (self.bank_id, item_id, topic_id, existing_version), + (self.bank_id, observation_id, topic_id, existing_version), ) occ_row = cur.fetchone() if occ_row: @@ -472,7 +472,7 @@ def save_episode(self, episode: Episode) -> None: ( new_occ, self.bank_id, - item_id, + observation_id, topic_id, existing_version, ), @@ -480,8 +480,8 @@ def save_episode(self, episode: Episode) -> None: # 6. Process DELETE mutations (observations not in current context) for mutation in episode.mutations: - if mutation.type == OpType.DELETE and mutation.item_id: - if mutation.item_id not in processed_item_ids: + if mutation.type == OpType.DELETE and mutation.observation_id: + if mutation.observation_id not in processed_observation_ids: # Get next version number cur.execute( """ @@ -489,7 +489,7 @@ def save_episode(self, episode: Episode) -> None: FROM observations WHERE bank_id = %s AND id = %s """, - (self.bank_id, mutation.item_id), + (self.bank_id, mutation.observation_id), ) row = cur.fetchone() version = row["next_ver"] if row else 1 @@ -502,7 +502,7 @@ def save_episode(self, episode: Episode) -> None: """, ( self.bank_id, - mutation.item_id, + mutation.observation_id, version, mutation.section, None, # content is NULL for DELETE @@ -532,7 +532,7 @@ def save_episode(self, episode: Episode) -> None: "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d)", episode.id, self.bank_id, episode.task, len(episode.context_memory.topics), - len(episode.context_memory.all_items()), + len(episode.context_memory.all_observations()), ) def load_context( @@ -541,7 +541,7 @@ def load_context( topic_ids: list[str] | None = None, topic_prefix: str | None = None, ) -> ContextMemory | None: - """Load context for a new episode: latest item versions from the + """Load context for a new episode: latest observation versions from the most recent episode for this (bank, task, topics). Args: @@ -559,7 +559,7 @@ def load_context( # Import here to avoid circular imports from codespy.agents.memory.hippocampus.context_memory import ( ContextMemory, - Item, + Observation, Topic, ) @@ -631,16 +631,16 @@ def load_context( """, (self.bank_id, episode_id), ) - items_by_id: dict[str, dict] = {} + observations_by_id: dict[str, dict] = {} for row in cur.fetchall(): - items_by_id[row["id"]] = { + observations_by_id[row["id"]] = { "section": row["type"], "content": row["content"], "version": row["version"], } # 4. Load observation-topic bindings for those latest versions - if items_by_id: + if observations_by_id: cur.execute( """ SELECT ot.observation_id, ot.topic_id, ot.observation_occurrence, ot.version_occurrence @@ -656,15 +656,15 @@ def load_context( """, (self.bank_id, self.bank_id, episode_id), ) - item_topics: dict[str, list[str]] = {} + observation_topics_map: dict[str, list[str]] = {} for row in cur.fetchall(): observation_id = row["observation_id"] - if observation_id not in item_topics: - item_topics[observation_id] = [] - item_topics[observation_id].append(row["topic_id"]) + if observation_id not in observation_topics_map: + observation_topics_map[observation_id] = [] + observation_topics_map[observation_id].append(row["topic_id"]) # Build ContextMemory sections - sections: dict[str, list[Item]] = { + sections: dict[str, list[Observation]] = { "context_roadmap": [], "context_understanding": [], "domain_constants": [], @@ -673,25 +673,25 @@ def load_context( "reusable_results": [], } - for item_id, item_data in items_by_id.items(): - section = item_data["section"] + for observation_id, observation_data in observations_by_id.items(): + section = observation_data["section"] if section not in sections: section = "context_understanding" # fallback - item = Item( - id=item_id, - content=item_data["content"], - topic_ids=item_topics.get(item_id, []), + obs = Observation( + id=observation_id, + content=observation_data["content"], + topic_ids=observation_topics_map.get(observation_id, []), ) - sections[section].append(item) + sections[section].append(obs) # Build ContextMemory ctx = ContextMemory(topics=topics) - for section_name, items in sections.items(): - setattr(ctx, section_name, items) + for section_name, observations in sections.items(): + setattr(ctx, section_name, observations) - total_items = sum(len(items) for items in sections.values()) - logger.debug("load_context: loaded %d topics, %d observations from episode %s", len(topics), total_items, episode_id) + total_observations = sum(len(observations) for observations in sections.values()) + logger.debug("load_context: loaded %d topics, %d observations from episode %s", len(topics), total_observations, episode_id) return ctx diff --git a/tests/test_context_memory.py b/tests/test_context_memory.py index 50fed0b..4dcf85d 100644 --- a/tests/test_context_memory.py +++ b/tests/test_context_memory.py @@ -7,7 +7,7 @@ from codespy.agents.memory.hippocampus import ( ContextMemory, - Item, + Observation, Operation, OpType, Topic, @@ -33,7 +33,7 @@ def test_validates_op_key(self): assert op.type == OpType.ADD def test_keyword_construction_unchanged(self): - op = Operation(type=OpType.DELETE, item_id="cu-abc") + op = Operation(type=OpType.DELETE, observation_id="cu-abc") assert op.type == OpType.DELETE def test_type_adapter_list_mixed_keys(self): @@ -42,8 +42,8 @@ def test_type_adapter_list_mixed_keys(self): ops = ta.validate_python( [ {"op": "ADD", "section": "domain_constants", "content": "test"}, - {"type": "DELETE", "item_id": "cu-123"}, - {"op": "REPLACE", "item_id": "cu-456", "content": "new"}, + {"type": "DELETE", "observation_id": "cu-123"}, + {"op": "REPLACE", "observation_id": "cu-456", "content": "new"}, ] ) assert [o.type for o in ops] == [OpType.ADD, OpType.DELETE, OpType.REPLACE] @@ -228,85 +228,85 @@ class TestContextMemoryApply: """Tests for ContextMemory.apply() method.""" def test_add_operation_with_topic_ids(self): - """ADD operation assigns topic_ids to new items.""" + """ADD operation assigns topic_ids to new observations.""" memory = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="Test")]) - ops = [Operation(type=OpType.ADD, section="context_understanding", content="New item")] + ops = [Operation(type=OpType.ADD, section="context_understanding", content="New observation")] 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"] + obs = new_memory.context_understanding[0] + assert obs.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"]), + Observation(id="cu-abc", content="Original", topic_ids=["t1"]), ], ) - ops = [Operation(type=OpType.REPLACE, item_id="cu-abc", content="Replaced")] + ops = [Operation(type=OpType.REPLACE, observation_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 + obs = new_memory.context_understanding[0] + assert obs.content == "Replaced" + assert obs.topic_ids == ["t1"] # Preserved, not overwritten def test_add_without_topic_ids(self): - """ADD without topic_ids creates item with empty topic_ids.""" + """ADD without topic_ids creates observation with empty topic_ids.""" memory = ContextMemory() - ops = [Operation(type=OpType.ADD, section="context_understanding", content="New item")] + ops = [Operation(type=OpType.ADD, section="context_understanding", content="New observation")] new_memory, _ = memory.apply(ops) - item = new_memory.context_understanding[0] - assert item.topic_ids == [] + obs = new_memory.context_understanding[0] + assert obs.topic_ids == [] - def test_delete_removes_item(self): - """DELETE operation removes item.""" + def test_delete_removes_observation(self): + """DELETE operation removes observation.""" memory = ContextMemory( context_understanding=[ - Item(id="cu-abc", content="To delete", topic_ids=["t1"]), + Observation(id="cu-abc", content="To delete", topic_ids=["t1"]), ], ) - ops = [Operation(type=OpType.DELETE, item_id="cu-abc")] + ops = [Operation(type=OpType.DELETE, observation_id="cu-abc")] new_memory, _ = memory.apply(ops) assert len(new_memory.context_understanding) == 0 def test_replace_nonexistent_valid_prefix_falls_back_to_add(self, caplog): - """REPLACE on non-existent item with valid prefix falls back to ADD.""" + """REPLACE on non-existent observation with valid prefix falls back to ADD.""" import logging memory = ContextMemory( context_understanding=[ - Item(id="cu-abc", content="Existing", topic_ids=["t1"]), + Observation(id="cu-abc", content="Existing", topic_ids=["t1"]), ], ) - ops = [Operation(type=OpType.REPLACE, item_id="cu-GONE", content="New content")] + ops = [Operation(type=OpType.REPLACE, observation_id="cu-GONE", content="New content")] with caplog.at_level( logging.INFO, logger="codespy.agents.memory.hippocampus.context_memory", ): new_memory, new_ids = memory.apply(ops, topic_ids=["t2"]) - assert len(new_ids) == 1 # fallback ADD created a new item + assert len(new_ids) == 1 # fallback ADD created a new observation assert new_memory.context_understanding[0].content == "Existing" # original untouched assert len(new_memory.context_understanding) == 2 # original + fallback - fallback_item = new_memory.context_understanding[1] - assert fallback_item.content == "New content" - assert fallback_item.topic_ids == ["t2"] # gets current topic_ids - assert fallback_item.id.startswith("cu-") # correct prefix + fallback_obs = new_memory.context_understanding[1] + assert fallback_obs.content == "New content" + assert fallback_obs.topic_ids == ["t2"] # gets current topic_ids + assert fallback_obs.id.startswith("cu-") # correct prefix assert "falling back to ADD" in caplog.text def test_replace_topic_id_skipped(self, caplog): - """REPLACE with topic-ID-shaped item_id (no prefix) logs warning and skips.""" + """REPLACE with topic-ID-shaped observation_id (no prefix) logs warning and skips.""" import logging memory = ContextMemory( context_understanding=[ - Item(id="cu-abc", content="Existing", topic_ids=["t1"]), + Observation(id="cu-abc", content="Existing", topic_ids=["t1"]), ], ) - ops = [Operation(type=OpType.REPLACE, item_id="owner/repo/package", content="New")] + ops = [Operation(type=OpType.REPLACE, observation_id="owner/repo/package", content="New")] with caplog.at_level( logging.WARNING, logger="codespy.agents.memory.hippocampus.context_memory", @@ -318,11 +318,11 @@ def test_replace_topic_id_skipped(self, caplog): assert "no valid prefix" in caplog.text def test_replace_url_topic_id_skipped(self, caplog): - """REPLACE with PR URL as item_id logs warning and skips.""" + """REPLACE with PR URL as observation_id logs warning and skips.""" import logging memory = ContextMemory() - ops = [Operation(type=OpType.REPLACE, item_id="https://github.com/o/r/pull/1", content="X")] + ops = [Operation(type=OpType.REPLACE, observation_id="https://github.com/o/r/pull/1", content="X")] with caplog.at_level( logging.WARNING, logger="codespy.agents.memory.hippocampus.context_memory", @@ -333,59 +333,6 @@ def test_replace_url_topic_id_skipped(self, caplog): assert "no valid prefix" in caplog.text -class TestContextMemoryMerge: - """Tests for ContextMemory.merge() method.""" - - def test_merge_deduplicates_topics(self): - """Merge deduplicates topics by ID.""" - mem1 = ContextMemory(topics=[Topic(id="t1", type="project_scope", description="First")]) - mem2 = ContextMemory(topics=[Topic(id="t1", type="project_scope", 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", type="project_scope", description="")]) - mem2 = ContextMemory(topics=[Topic(id="t1", type="project_scope", 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", type="project_scope", description="T1")], - context_understanding=[Item(id="cu-1", content="Item 1", topic_ids=["t1"])], - ) - mem2 = ContextMemory( - topics=[Topic(id="t2", type="project_scope", description="T2")], - context_understanding=[Item(id="cu-2", content="Item 2", topic_ids=["t2"])], - ) - mem3 = ContextMemory( - topics=[Topic(id="t3", type="project_scope", 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.""" diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py index 33b04fa..7f5ab90 100644 --- a/tests/test_hippocampus.py +++ b/tests/test_hippocampus.py @@ -13,8 +13,9 @@ from codespy.agents.memory.hippocampus import ( ContextMemory, Hippocampus, - Item, Mutation, + Observation, + ObservationTag, Operation, OpType, Topic, @@ -70,7 +71,7 @@ def test_save_simple_episode(self, episode_store): ctx = ContextMemory( topics=[Topic(id="test/topic", type="project_scope", description="Test topic")], context_understanding=[ - Item(id="cu-1", content="Test item", topic_ids=["test/topic"]) + Observation(id="cu-1", content="Test observation", topic_ids=["test/topic"]) ], ) episode = Episode( @@ -104,7 +105,7 @@ def test_save_and_load_episode_roundtrip(self, episode_store): ctx = ContextMemory( topics=[Topic(id="owner/repo/pkg", type="project_scope", description="Test package")], context_understanding=[ - Item(id="cu-abc123", content="Test understanding", topic_ids=["owner/repo/pkg"]) + Observation(id="cu-abc123", content="Test understanding", topic_ids=["owner/repo/pkg"]) ], ) episode = Episode( @@ -134,20 +135,20 @@ def test_save_and_load_episode_roundtrip(self, episode_store): assert loaded_ctx.context_understanding[0].content == "Test understanding" -class TestEpisodeStoreItemVersioning: - """Tests for item versioning on REPLACE operations.""" +class TestEpisodeStoreObservationVersioning: + """Tests for observation versioning on REPLACE operations.""" - def test_item_versioning_on_replace(self, episode_store): - """Item versions should increment on REPLACE operations.""" + def test_observation_versioning_on_replace(self, episode_store): + """Observation versions should increment on REPLACE operations.""" import uuid topic_id = "owner/repo" - # Episode 1: Add an item + # Episode 1: Add an observation ctx1 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id="cu-item1", content="Original content", topic_ids=[topic_id]) + Observation(id="cu-obs1", content="Original content", topic_ids=[topic_id]) ], ) episode1 = Episode( @@ -162,7 +163,7 @@ def test_item_versioning_on_replace(self, episode_store): Mutation( step=0, type=OpType.ADD, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", content="Original content", previous_content=None, @@ -173,11 +174,11 @@ def test_item_versioning_on_replace(self, episode_store): ) episode_store.save_episode(episode1) - # Episode 2: Replace the item + # Episode 2: Replace the observation ctx2 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id="cu-item1", content="Updated content", topic_ids=[topic_id]) + Observation(id="cu-obs1", content="Updated content", topic_ids=[topic_id]) ], ) episode2 = Episode( @@ -192,7 +193,7 @@ def test_item_versioning_on_replace(self, episode_store): Mutation( step=0, type=OpType.REPLACE, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", content="Updated content", previous_content="Original content", @@ -213,18 +214,18 @@ def test_item_versioning_on_replace(self, episode_store): assert loaded_ctx is not None assert loaded_ctx.context_understanding[0].content == "Updated content" - def test_inherited_items_preserved(self, episode_store): - """Inherited items (no mutation) should preserve their content.""" + def test_inherited_observations_preserved(self, episode_store): + """Inherited observations (no mutation) should preserve their content.""" import uuid topic_id = "owner/repo" - # Episode 1: Add two items + # Episode 1: Add two observations ctx1 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id="cu-item1", content="Item 1 content", topic_ids=[topic_id]), - Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), + Observation(id="cu-obs1", content="Observation 1 content", topic_ids=[topic_id]), + Observation(id="cu-obs2", content="Observation 2 content", topic_ids=[topic_id]), ], ) episode1 = Episode( @@ -239,18 +240,18 @@ def test_inherited_items_preserved(self, episode_store): Mutation( step=0, type=OpType.ADD, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", - content="Item 1 content", + content="Observation 1 content", previous_content=None, topic_ids=[topic_id], ), Mutation( step=0, type=OpType.ADD, - item_id="cu-item2", + observation_id="cu-obs2", section="context_understanding", - content="Item 2 content", + content="Observation 2 content", previous_content=None, topic_ids=[topic_id], ), @@ -259,12 +260,12 @@ def test_inherited_items_preserved(self, episode_store): ) episode_store.save_episode(episode1) - # Episode 2: Only replace item1, item2 is inherited + # Episode 2: Only replace obs1, obs2 is inherited ctx2 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id="cu-item1", content="Item 1 updated", topic_ids=[topic_id]), - Item(id="cu-item2", content="Item 2 content", topic_ids=[topic_id]), # inherited + Observation(id="cu-obs1", content="Observation 1 updated", topic_ids=[topic_id]), + Observation(id="cu-obs2", content="Observation 2 content", topic_ids=[topic_id]), # inherited ], ) episode2 = Episode( @@ -279,10 +280,10 @@ def test_inherited_items_preserved(self, episode_store): Mutation( step=0, type=OpType.REPLACE, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", - content="Item 1 updated", - previous_content="Item 1 content", + content="Observation 1 updated", + previous_content="Observation 1 content", topic_ids=[topic_id], ), ], @@ -296,11 +297,11 @@ def test_inherited_items_preserved(self, episode_store): topic_ids=[topic_id], ) - # Both items should be present with correct content + # Both observations should be present with correct content assert loaded_ctx is not None - items_by_id = {item.id: item for item in loaded_ctx.context_understanding} - assert items_by_id["cu-item1"].content == "Item 1 updated" - assert items_by_id["cu-item2"].content == "Item 2 content" + observations_by_id = {obs.id: obs for obs in loaded_ctx.context_understanding} + assert observations_by_id["cu-obs1"].content == "Observation 1 updated" + assert observations_by_id["cu-obs2"].content == "Observation 2 content" class TestEpisodeStoreWithHippocampus: @@ -345,7 +346,7 @@ def test_load_context_returns_latest_episode(self, episode_store): ctx = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id=f"cu-item{i}", content=f"Content {i}", topic_ids=[topic_id]) + Observation(id=f"cu-obs{i}", content=f"Content {i}", topic_ids=[topic_id]) ], ) episode = Episode( @@ -369,23 +370,23 @@ def test_load_context_returns_latest_episode(self, episode_store): # Should have the latest content (from episode 2) assert loaded_ctx is not None - assert len(loaded_ctx.context_understanding) == 3 # All items accumulated + assert len(loaded_ctx.context_understanding) == 3 # All observations accumulated class TestEpisodeStoreDeleteTombstone: """Tests for DELETE operations (tombstone handling).""" def test_delete_creates_tombstone(self, episode_store): - """DELETE should create a tombstone version of the item.""" + """DELETE should create a tombstone version of the observation.""" import uuid topic_id = "owner/repo" - # Episode 1: Add an item + # Episode 1: Add an observation ctx1 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[ - Item(id="cu-item1", content="To be deleted", topic_ids=[topic_id]) + Observation(id="cu-obs1", content="To be deleted", topic_ids=[topic_id]) ], ) episode1 = Episode( @@ -400,7 +401,7 @@ def test_delete_creates_tombstone(self, episode_store): Mutation( step=0, type=OpType.ADD, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", content="To be deleted", previous_content=None, @@ -411,7 +412,7 @@ def test_delete_creates_tombstone(self, episode_store): ) episode_store.save_episode(episode1) - # Episode 2: Delete the item (item not in context_memory, but in mutations) + # Episode 2: Delete the observation (observation not in context_memory, but in mutations) ctx2 = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], context_understanding=[], # Empty after delete @@ -428,7 +429,7 @@ def test_delete_creates_tombstone(self, episode_store): Mutation( step=0, type=OpType.DELETE, - item_id="cu-item1", + observation_id="cu-obs1", section="context_understanding", content=None, # DELETE has no new content previous_content="To be deleted", @@ -439,13 +440,13 @@ def test_delete_creates_tombstone(self, episode_store): ) episode_store.save_episode(episode2) - # Load the latest context - deleted item should not appear + # Load the latest context - deleted observation should not appear loaded_ctx = episode_store.load_context( task="code_review", topic_ids=[topic_id], ) - # Should have no items (deleted) + # Should have no observations (deleted) assert loaded_ctx is not None assert len(loaded_ctx.context_understanding) == 0 @@ -462,12 +463,12 @@ def _make_hip(topic_ids=None, distill_step=0): return hip def test_add_backfill_with_mixed_ops(self): - """ADD item_ids back-filled correctly when DELETEs precede them.""" + """ADD observation_ids back-filled correctly when DELETEs precede them.""" pre = ContextMemory( - context_understanding=[Item(id="cu-existing", content="Old", topic_ids=["t1"])], + context_understanding=[Observation(id="cu-existing", content="Old", topic_ids=["t1"])], ) ops = [ - Operation(type=OpType.DELETE, item_id="cu-existing"), + Operation(type=OpType.DELETE, observation_id="cu-existing"), Operation(type=OpType.ADD, section="context_understanding", content="New 1"), Operation(type=OpType.ADD, section="domain_constants", content="New 2"), ] @@ -478,17 +479,17 @@ def test_add_backfill_with_mixed_ops(self): assert len(mutations) == 3 assert mutations[0].type == OpType.DELETE - assert mutations[0].item_id == "cu-existing" + assert mutations[0].observation_id == "cu-existing" assert mutations[1].type == OpType.ADD - assert mutations[1].item_id == "cu-aaa" + assert mutations[1].observation_id == "cu-aaa" assert mutations[2].type == OpType.ADD - assert mutations[2].item_id == "dc-bbb" + assert mutations[2].observation_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.DELETE, observation_id="cu-ghost"), Operation(type=OpType.ADD, section="context_understanding", content="New"), ] new_ids = ["cu-xyz"] @@ -498,7 +499,7 @@ def test_add_backfill_skipped_delete(self): assert len(mutations) == 1 assert mutations[0].type == OpType.ADD - assert mutations[0].item_id == "cu-xyz" + assert mutations[0].observation_id == "cu-xyz" def test_add_backfill_all_adds(self): """All-ADD batch back-fills in order.""" @@ -513,7 +514,7 @@ def test_add_backfill_all_adds(self): 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 [m.observation_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): @@ -529,8 +530,8 @@ def test_add_backfill_length_mismatch_raises(self): hip._record_mutations(ops, new_ids, pre) -class TestUpdateItemScores: - """Unit tests for _update_item_scores scoring logic.""" +class TestUpdateObservationScores: + """Unit tests for _update_observation_scores scoring logic.""" @staticmethod def _make_hip(): @@ -541,39 +542,33 @@ def _make_hip(): def test_helpful_increments(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag - hip._update_item_scores({"a": ItemTag.HELPFUL}) + hip._update_observation_scores({"a": ObservationTag.HELPFUL}) assert hip.scores == {"a": 1} def test_helpful_accumulates(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag hip.scores = {"a": 3} - hip._update_item_scores({"a": ItemTag.HELPFUL}) + hip._update_observation_scores({"a": ObservationTag.HELPFUL}) assert hip.scores == {"a": 4} def test_harmful_decrements(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag hip.scores = {"a": 2} - hip._update_item_scores({"a": ItemTag.HARMFUL}) + hip._update_observation_scores({"a": ObservationTag.HARMFUL}) assert hip.scores == {"a": 1} def test_stale_decrements(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag - hip._update_item_scores({"a": ItemTag.STALE}) + hip._update_observation_scores({"a": ObservationTag.STALE}) assert hip.scores == {"a": -1} def test_neutral_initializes_zero(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag - hip._update_item_scores({"a": ItemTag.NEUTRAL}) + hip._update_observation_scores({"a": ObservationTag.NEUTRAL}) assert hip.scores == {"a": 0} def test_neutral_preserves_existing(self): hip = self._make_hip() - from codespy.agents.memory.hippocampus import ItemTag hip.scores = {"a": 5} - hip._update_item_scores({"a": ItemTag.NEUTRAL}) + hip._update_observation_scores({"a": ObservationTag.NEUTRAL}) assert hip.scores == {"a": 5} From 226229774c2dee33be349810093d92670316a8a7 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 10 Sep 2026 14:51:01 +0200 Subject: [PATCH 09/14] schemas --- docs/memory.md | 11 --- .../agents/memory/hippocampus/budget.py | 2 +- src/codespy/agents/memory/postgres.py | 86 ++++++------------- tests/test_hippocampus.py | 48 ++++++++--- 4 files changed, 64 insertions(+), 83 deletions(-) diff --git a/docs/memory.md b/docs/memory.md index d41ebf8..8fcb478 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -129,13 +129,6 @@ erDiagram int version_occurrence "DEFAULT 0" } - episode_observations { - varchar(64) bank_id PK, FK - uuid episode_id PK, FK - varchar(48) observation_id PK, FK - int observation_version FK - } - artifacts { varchar(64) bank_id PK, FK uuid episode_id PK, FK @@ -150,8 +143,6 @@ erDiagram topics ||--o{ episode_topics : "tags" episodes ||--o{ observations : "creates" episodes ||--o{ artifacts : "produces" - episodes ||--o{ episode_observations : "references" - observations ||--o{ episode_observations : "referenced by" observations ||--o{ observation_topics : "scoped to" topics ||--o{ observation_topics : "scopes" ``` @@ -165,14 +156,12 @@ erDiagram | `idx_episode_topics_reverse` | episode_topics | `(bank_id, topic_id)` | | `idx_observations_episode` | observations | `(bank_id, episode_id)` | | `idx_observation_topics_reverse` | observation_topics | `(bank_id, topic_id)` | -| `idx_episode_observations_reverse` | episode_observations | `(bank_id, observation_id)` | ### Notes - All tables cascade-delete from `banks` - `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE history per observation - `episode_topics` and `observation_topics` are M:N junction tables -- `episode_observations` links episodes to the specific observation versions they reference - `observation_topics.observation_occurrence` counts cumulative topic associations across all versions; `version_occurrence` counts within one version - Requires PostgreSQL extension: `pg_trgm` diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 3b83c1c..85b56e4 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -127,7 +127,7 @@ def evict(context_memory: ContextMemory, scores: dict[str, int], budget: int) -> item_section: dict[str, str] = { it.id: sec for sec in context_memory.section_names() for it in context_memory.section(sec) } - flat = context_memory.all_items() + flat = context_memory.all_observations() order = {it.id: i for i, it in enumerate(flat)} victims = sorted( flat, diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index 36204d8..79f3c2d 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -176,24 +176,8 @@ def ensure_schema(self) -> None: ON observation_topics (bank_id, topic_id) """) - # Episode observations junction - cur.execute(""" - CREATE TABLE IF NOT EXISTS episode_observations ( - bank_id VARCHAR(64) NOT NULL, - episode_id UUID NOT NULL, - observation_id VARCHAR(48) NOT NULL, - observation_version INT NOT NULL, - PRIMARY KEY (bank_id, episode_id, observation_id), - FOREIGN KEY (bank_id, episode_id) - REFERENCES episodes(bank_id, id) ON DELETE CASCADE, - FOREIGN KEY (bank_id, observation_id, observation_version) - REFERENCES observations(bank_id, id, version) ON DELETE CASCADE - ) - """) - cur.execute(""" - CREATE INDEX IF NOT EXISTS idx_episode_observations_reverse - ON episode_observations (bank_id, observation_id) - """) + # Episode observations junction (dropped - no longer needed) + cur.execute("DROP TABLE IF EXISTS episode_observations") # Artifacts table cur.execute(""" @@ -369,16 +353,6 @@ def save_episode(self, episode: Episode) -> None: ), ) - # Insert episode_observations junction - cur.execute( - """ - INSERT INTO episode_observations (bank_id, episode_id, observation_id, observation_version) - VALUES (%s, %s, %s, %s) - ON CONFLICT (bank_id, episode_id, observation_id) DO NOTHING - """, - (self.bank_id, str(episode.id), observation_id, version), - ) - # Insert observation_topics for this version for topic_id in obs.topic_ids: # Count occurrences @@ -433,21 +407,6 @@ def save_episode(self, episode: Episode) -> None: row = cur.fetchone() existing_version = row["max_ver"] if row and row["max_ver"] else 1 - # Insert episode_observations junction with existing version - cur.execute( - """ - INSERT INTO episode_observations (bank_id, episode_id, observation_id, observation_version) - VALUES (%s, %s, %s, %s) - ON CONFLICT (bank_id, episode_id, observation_id) DO NOTHING - """, - ( - self.bank_id, - str(episode.id), - observation_id, - existing_version, - ), - ) - # Increment observation_occurrence for inherited observations for topic_id in obs.topic_ids: cur.execute( @@ -513,7 +472,6 @@ def save_episode(self, episode: Episode) -> None: 0, ), ) - # Note: DELETE observations are NOT inserted into episode_observations # 7. Insert artifacts for name, content in (episode.artifacts or {}).items(): @@ -619,17 +577,28 @@ def load_context( for row in cur.fetchall() ] - # 3. Load observations at their LATEST version (not the pinned version) + # 3. Load observations at their LATEST version via topic bindings + task filter + # Exclude observations whose latest version is a DELETE tombstone + topic_id_list = [t.id for t in topics] cur.execute( """ - SELECT DISTINCT ON (o.id) o.id, o.type, o.content, o.version - FROM episode_observations eo - JOIN observations o ON o.bank_id = eo.bank_id AND o.id = eo.observation_id - WHERE eo.bank_id = %s AND eo.episode_id = %s + SELECT o.id, o.type, o.content, o.version + FROM observations o + JOIN episodes e ON e.bank_id = o.bank_id AND e.id = o.episode_id + WHERE o.bank_id = %s + AND e.task = %s + AND EXISTS ( + SELECT 1 FROM observation_topics ot + WHERE ot.bank_id = o.bank_id AND ot.observation_id = o.id + AND ot.topic_id = ANY(%s) + ) + AND o.version = ( + SELECT MAX(o2.version) FROM observations o2 + WHERE o2.bank_id = o.bank_id AND o2.id = o.id + ) AND o.op_type != 'DELETE' - ORDER BY o.id, o.version DESC """, - (self.bank_id, episode_id), + (self.bank_id, task, topic_id_list), ) observations_by_id: dict[str, dict] = {} for row in cur.fetchall(): @@ -646,15 +615,16 @@ def load_context( SELECT ot.observation_id, ot.topic_id, ot.observation_occurrence, ot.version_occurrence FROM observation_topics ot WHERE ot.bank_id = %s - AND (ot.observation_id, ot.observation_version) IN ( - SELECT o.id, MAX(o.version) - FROM episode_observations eo - JOIN observations o ON o.bank_id = eo.bank_id AND o.id = eo.observation_id - WHERE eo.bank_id = %s AND eo.episode_id = %s AND o.op_type != 'DELETE' - GROUP BY o.id + AND ot.observation_id = ANY(%s) + AND ot.observation_version = ( + SELECT MAX(o2.version) + FROM observations o2 + WHERE o2.bank_id = ot.bank_id + AND o2.id = ot.observation_id + AND o2.op_type != 'DELETE' ) """, - (self.bank_id, self.bank_id, episode_id), + (self.bank_id, list(observations_by_id.keys())), ) observation_topics_map: dict[str, list[str]] = {} for row in cur.fetchall(): diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py index 7f5ab90..854e2ad 100644 --- a/tests/test_hippocampus.py +++ b/tests/test_hippocampus.py @@ -101,11 +101,13 @@ def test_save_and_load_episode_roundtrip(self, episode_store): """Should save and load an episode successfully.""" import uuid - # Create and save an episode + topic_id = "owner/repo/pkg" + + # Create and save an episode with ADD mutation so observation persists ctx = ContextMemory( - topics=[Topic(id="owner/repo/pkg", type="project_scope", description="Test package")], + topics=[Topic(id=topic_id, type="project_scope", description="Test package")], context_understanding=[ - Observation(id="cu-abc123", content="Test understanding", topic_ids=["owner/repo/pkg"]) + Observation(id="cu-abc123", content="Test understanding", topic_ids=[topic_id]) ], ) episode = Episode( @@ -116,7 +118,17 @@ def test_save_and_load_episode_roundtrip(self, episode_store): context_memory=ctx, timestamp=datetime.now(UTC), run_id="run-456", - mutations=[], + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + observation_id="cu-abc123", + section="context_understanding", + content="Test understanding", + previous_content=None, + topic_ids=[topic_id], + ) + ], artifacts={"review": "LGTM"}, ) episode_store.save_episode(episode) @@ -124,13 +136,13 @@ def test_save_and_load_episode_roundtrip(self, episode_store): # Load context for the same task and topic loaded_ctx = episode_store.load_context( task="code_review", - topic_ids=["owner/repo/pkg"], + topic_ids=[topic_id], ) # Should have loaded the context assert loaded_ctx is not None assert len(loaded_ctx.topics) == 1 - assert loaded_ctx.topics[0].id == "owner/repo/pkg" + assert loaded_ctx.topics[0].id == topic_id assert len(loaded_ctx.context_understanding) == 1 assert loaded_ctx.context_understanding[0].content == "Test understanding" @@ -142,7 +154,7 @@ def test_observation_versioning_on_replace(self, episode_store): """Observation versions should increment on REPLACE operations.""" import uuid - topic_id = "owner/repo" + topic_id = "owner/repo/versioning-test" # Episode 1: Add an observation ctx1 = ContextMemory( @@ -218,7 +230,7 @@ def test_inherited_observations_preserved(self, episode_store): """Inherited observations (no mutation) should preserve their content.""" import uuid - topic_id = "owner/repo" + topic_id = "owner/repo/inherited-test" # Episode 1: Add two observations ctx1 = ContextMemory( @@ -339,9 +351,9 @@ def test_load_context_returns_latest_episode(self, episode_store): """load_context should return the context from the latest episode.""" import uuid - topic_id = "owner/repo" + topic_id = "owner/repo/latest-episode-test" - # Save multiple episodes + # Save multiple episodes with ADD mutations so observations persist for i in range(3): ctx = ContextMemory( topics=[Topic(id=topic_id, type="project_scope", description="Test repo")], @@ -357,7 +369,17 @@ def test_load_context_returns_latest_episode(self, episode_store): context_memory=ctx, timestamp=datetime.now(UTC), run_id=f"run-{i}", - mutations=[], + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + observation_id=f"cu-obs{i}", + section="context_understanding", + content=f"Content {i}", + previous_content=None, + topic_ids=[topic_id], + ) + ], artifacts={}, ) episode_store.save_episode(episode) @@ -368,7 +390,7 @@ def test_load_context_returns_latest_episode(self, episode_store): topic_ids=[topic_id], ) - # Should have the latest content (from episode 2) + # Should have all 3 observations (from ADD mutations) assert loaded_ctx is not None assert len(loaded_ctx.context_understanding) == 3 # All observations accumulated @@ -380,7 +402,7 @@ def test_delete_creates_tombstone(self, episode_store): """DELETE should create a tombstone version of the observation.""" import uuid - topic_id = "owner/repo" + topic_id = "owner/repo/delete-test" # Episode 1: Add an observation ctx1 = ContextMemory( From 1d75b6cf2573152490a3ca4037fd9a70e71e1752 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Thu, 10 Sep 2026 15:48:21 +0200 Subject: [PATCH 10/14] schemas --- .env.example | 21 +++-- action.yml | 35 +++++++- codespy.yaml | 27 +++--- docs/configuration.md | 7 +- docs/memory.md | 27 ++++-- src/codespy/config_memory.py | 112 +++++++++++++++++------- tests/test_config_memory.py | 160 +++++++++++++++++++++++++++++++---- 7 files changed, 308 insertions(+), 81 deletions(-) diff --git a/.env.example b/.env.example index 42480dd..6cb0e32 100644 --- a/.env.example +++ b/.env.example @@ -188,19 +188,24 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # Episodes are stored in PostgreSQL using a relational schema with pgvector # for semantic search. # -# Production uses MEMORY_POSTGRES_URI to connect to an external PostgreSQL -# instance (AWS RDS, etc.). For local development, pg0-embedded is used -# if MEMORY_POSTGRES_URI is not set. - -# PostgreSQL URI — set for production (AWS RDS, etc.) -# When unset, pg0-embedded auto-starts a local PostgreSQL instance. -# MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy +# Production uses MEMORY_POSTGRES_HOST (+ credentials) to connect to an +# external PostgreSQL instance (AWS RDS, etc.). For local development, +# pg0-embedded is used if MEMORY_POSTGRES_HOST is not set. + +# PostgreSQL connection — set for production (AWS RDS, etc.) +# When MEMORY_POSTGRES_HOST is unset, pg0-embedded auto-starts a local instance. +# MEMORY_POSTGRES_HOST=rds-host.amazonaws.com +# MEMORY_POSTGRES_PORT=5432 +# MEMORY_POSTGRES_USER=myuser +# MEMORY_POSTGRES_PASSWORD=mypassword +# MEMORY_POSTGRES_DATABASE=codespy +# MEMORY_POSTGRES_SCHEMA=public # PostgreSQL search_path (optional) # Bank ID — nickname, username, email, or agent name scoping all memory data. # When unset, defaults to "codespy". # MEMORY_BANK_ID=my-agent -# pg0 embedded settings (local dev only, ignored when MEMORY_POSTGRES_URI is set) +# pg0 embedded settings (local dev only, ignored when MEMORY_POSTGRES_HOST is set) # MEMORY_PG0_NAME=codespy # MEMORY_PG0_PORT=5432 # MEMORY_PG0_DATA_DIR=~/.cache/codespy/pg0 # default; persists on Docker-mounted codespy-cache volume diff --git a/action.yml b/action.yml index ef833db..5cae683 100644 --- a/action.yml +++ b/action.yml @@ -314,8 +314,25 @@ inputs: required: false default: 'false' - memory-postgres-uri: - description: 'PostgreSQL connection URI for memory storage (e.g., postgresql://user:pass@host:5432/codespy). When unset, pg0-embedded auto-starts a local instance inside Docker.' + memory-postgres-host: + description: 'PostgreSQL host for memory storage (e.g., rds-host.amazonaws.com). When unset, pg0-embedded auto-starts.' + required: false + memory-postgres-port: + description: 'PostgreSQL port (default: 5432)' + required: false + default: '5432' + memory-postgres-user: + description: 'PostgreSQL user (default: postgres)' + required: false + memory-postgres-password: + description: 'PostgreSQL password' + required: false + memory-postgres-database: + description: 'PostgreSQL database name (default: codespy)' + required: false + default: 'codespy' + memory-postgres-schema: + description: 'PostgreSQL schema / search_path (optional)' required: false memory-bank-id: @@ -528,7 +545,12 @@ runs: # Memory (Hippocampus) MEMORY_DEFAULT_ENABLED: ${{ inputs.memory-enabled }} - MEMORY_POSTGRES_URI: ${{ inputs.memory-postgres-uri }} + MEMORY_POSTGRES_HOST: ${{ inputs.memory-postgres-host }} + MEMORY_POSTGRES_PORT: ${{ inputs.memory-postgres-port }} + MEMORY_POSTGRES_USER: ${{ inputs.memory-postgres-user }} + MEMORY_POSTGRES_PASSWORD: ${{ inputs.memory-postgres-password }} + MEMORY_POSTGRES_DATABASE: ${{ inputs.memory-postgres-database }} + MEMORY_POSTGRES_SCHEMA: ${{ inputs.memory-postgres-schema }} MEMORY_BANK_ID: ${{ inputs.memory-bank-id }} MEMORY_DEFAULT_MAX_REFLECTS: ${{ inputs.memory-max-reflects }} MEMORY_DISTILLER_MODEL: ${{ inputs.memory-distiller-model }} @@ -633,7 +655,12 @@ runs: # Memory (Hippocampus) [ -n "$MEMORY_DEFAULT_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DEFAULT_ENABLED" - [ -n "$MEMORY_POSTGRES_URI" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_URI" + [ -n "$MEMORY_POSTGRES_HOST" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_HOST" + [ -n "$MEMORY_POSTGRES_PORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_PORT" + [ -n "$MEMORY_POSTGRES_USER" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_USER" + [ -n "$MEMORY_POSTGRES_PASSWORD" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_PASSWORD" + [ -n "$MEMORY_POSTGRES_DATABASE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_DATABASE" + [ -n "$MEMORY_POSTGRES_SCHEMA" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_POSTGRES_SCHEMA" [ -n "$MEMORY_BANK_ID" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_BANK_ID" [ -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" diff --git a/codespy.yaml b/codespy.yaml index ead5af0..fd5befe 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -74,23 +74,30 @@ gitlab: # default globally; enabled by default for summary — see `memory:` # blocks under each signature below. -# Episodes are persisted in PostgreSQL. When no postgres_uri is set, +# Episodes are persisted in PostgreSQL. When postgres.host is unset, # pg0-embedded auto-starts a local PostgreSQL instance (zero config). -# For production, set postgres_uri to an external PostgreSQL (AWS RDS, etc.). +# For production, set postgres.host to an external PostgreSQL (AWS RDS, etc.). memory: - # PostgreSQL connection URI for production (AWS RDS, etc.). - # When unset, pg0-embedded auto-starts a local PostgreSQL instance. - postgres_uri: null # MEMORY_POSTGRES_URI + # External PostgreSQL (production, AWS RDS, etc.). + # When postgres.host is unset, pg0-embedded auto-starts a local instance. + postgres: + host: null # MEMORY_POSTGRES_HOST + port: 5432 # MEMORY_POSTGRES_PORT + user: null # MEMORY_POSTGRES_USER (default: postgres) + password: null # MEMORY_POSTGRES_PASSWORD + database: codespy # MEMORY_POSTGRES_DATABASE + schema: null # MEMORY_POSTGRES_SCHEMA (search_path; null = server default) + + # pg0-embedded settings (local dev only, ignored when postgres.host is set) + pg0: + name: codespy # MEMORY_PG0_NAME + port: null # MEMORY_PG0_PORT (auto-detected if unset) + data_dir: null # MEMORY_PG0_DATA_DIR (default: ~/.cache/codespy/pg0) # Bank ID — scopes all memory data. Can be a nickname, username, email, # or agent name. When unset, auto-generated from hostname. bank_id: null # MEMORY_BANK_ID - # pg0-embedded settings (local dev only, ignored when postgres_uri is set) - pg0_name: codespy # MEMORY_PG0_NAME - pg0_port: null # MEMORY_PG0_PORT (auto-detected if unset) - pg0_data_dir: null # MEMORY_PG0_DATA_DIR (default: ~/.cache/codespy/pg0) - # 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) diff --git a/docs/configuration.md b/docs/configuration.md index f0f1eed..6314e2a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -172,7 +172,12 @@ Brief overview: | Setting | Env Var | Default | Description | |---------|---------|---------|-------------| -| PostgreSQL URI | `MEMORY_POSTGRES_URI` | — | External PostgreSQL connection URI | +| PostgreSQL host | `MEMORY_POSTGRES_HOST` | — | External PostgreSQL host | +| PostgreSQL port | `MEMORY_POSTGRES_PORT` | `5432` | External PostgreSQL port | +| PostgreSQL user | `MEMORY_POSTGRES_USER` | `postgres` | External PostgreSQL user | +| PostgreSQL password | `MEMORY_POSTGRES_PASSWORD` | — | External PostgreSQL password | +| PostgreSQL database | `MEMORY_POSTGRES_DATABASE` | `codespy` | External PostgreSQL database | +| PostgreSQL schema | `MEMORY_POSTGRES_SCHEMA` | — | PostgreSQL search_path (optional) | | Bank ID | `MEMORY_BANK_ID` | `codespy` | Scopes all memory data | | pg0 name | `MEMORY_PG0_NAME` | `codespy` | pg0-embedded database name (local dev) | | pg0 port | `MEMORY_PG0_PORT` | auto | pg0-embedded port (local dev) | diff --git a/docs/memory.md b/docs/memory.md index 8fcb478..3e4060e 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -29,7 +29,7 @@ and artifacts cascade-delete from a bank. - An Episode captures one agent's run: task, context_memory, mutations, artifacts, timestamp - Stored in PostgreSQL (auto-created tables). pg0-embedded auto-starts - a local instance when no `MEMORY_POSTGRES_URI` is set. + a local instance when no `MEMORY_POSTGRES_HOST` is set. - `EpisodeStore.load_context(task, topic_ids, topic_prefix)` retrieves the latest context by `timestamp DESC`, filtering on bank + task + topic @@ -204,11 +204,16 @@ Observation capacity ≈ context_memory_tokens / observation_tokens (16384/512 = | Env Var | YAML Path | Default | Description | |---------|-----------|---------|-------------| -| `MEMORY_POSTGRES_URI` | `memory.postgres_uri` | — | External PostgreSQL connection URI | +| `MEMORY_POSTGRES_HOST` | `memory.postgres.host` | — | External PostgreSQL host | +| `MEMORY_POSTGRES_PORT` | `memory.postgres.port` | `5432` | External PostgreSQL port | +| `MEMORY_POSTGRES_USER` | `memory.postgres.user` | `postgres` | External PostgreSQL user | +| `MEMORY_POSTGRES_PASSWORD` | `memory.postgres.password` | — | External PostgreSQL password | +| `MEMORY_POSTGRES_DATABASE` | `memory.postgres.database` | `codespy` | External PostgreSQL database | +| `MEMORY_POSTGRES_SCHEMA` | `memory.postgres.schema` | — | PostgreSQL search_path | +| `MEMORY_PG0_NAME` | `memory.pg0.name` | `codespy` | pg0-embedded database name | +| `MEMORY_PG0_PORT` | `memory.pg0.port` | auto | pg0-embedded port | +| `MEMORY_PG0_DATA_DIR` | `memory.pg0.data_dir` | — | Custom data directory for pg0-embedded | | `MEMORY_BANK_ID` | `memory.bank_id` | `codespy` | Scopes all memory data | -| `MEMORY_PG0_NAME` | `memory.pg0_name` | `codespy` | pg0-embedded database name | -| `MEMORY_PG0_PORT` | `memory.pg0_port` | auto | pg0-embedded port | -| `MEMORY_PG0_DATA_DIR` | `memory.pg0_data_dir` | — | Custom data directory for pg0-embedded | | `MEMORY_DEFAULT_ENABLED` | `memory.default_enabled` | `false` | Enable memory globally | | `MEMORY_DEFAULT_MAX_REFLECTS` | `memory.default_max_reflects` | `0` | Reflection iterations | | `MEMORY_COMPACT_TRAJECTORY` | `memory.compact_trajectory` | `true` | Apply head+tail trajectory bounding before distillation | @@ -250,9 +255,11 @@ MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ``` > **Note:** pg0-embedded auto-starts when installed (default for local dev). -> For production, set `MEMORY_POSTGRES_URI`: +> For production, set `MEMORY_POSTGRES_HOST`: > ``` -> MEMORY_POSTGRES_URI=postgresql://user:pass@host:5432/codespy +> MEMORY_POSTGRES_HOST=rds-host.amazonaws.com +> MEMORY_POSTGRES_USER=myuser +> MEMORY_POSTGRES_PASSWORD=mypassword > ``` ### GitHub Action @@ -265,13 +272,15 @@ MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # Memory with external PostgreSQL memory-enabled: 'true' - memory-postgres-uri: ${{ secrets.MEMORY_POSTGRES_URI }} + memory-postgres-host: ${{ secrets.MEMORY_POSTGRES_HOST }} + memory-postgres-user: ${{ secrets.MEMORY_POSTGRES_USER }} + memory-postgres-password: ${{ secrets.MEMORY_POSTGRES_PASSWORD }} memory-distiller-model: 'anthropic/claude-haiku-4-5-20251001' memory-cartographer-model: 'anthropic/claude-haiku-4-5-20251001' ``` > **Note:** pg0-embedded is included in the Docker image. For persistent memory -> across CI runs, use an external PostgreSQL instance via `memory-postgres-uri`. +> across CI runs, use an external PostgreSQL instance via `memory-postgres-host`. --- diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 4f0810e..63e5fd1 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -56,6 +56,35 @@ class LLMSettings(BaseModel): max_tokens: int +class PostgresConfig(BaseModel): + """External PostgreSQL connection settings (production).""" + host: str | None = None # MEMORY_POSTGRES_HOST + port: int = 5432 # MEMORY_POSTGRES_PORT + user: str | None = None # MEMORY_POSTGRES_USER + password: str | None = None # MEMORY_POSTGRES_PASSWORD + database: str = "codespy" # MEMORY_POSTGRES_DATABASE + schema: str | None = None # MEMORY_POSTGRES_SCHEMA (search_path) + + def build_uri(self) -> str | None: + """Build a psycopg connection URI. Returns None when host is unset.""" + if not self.host: + return None + from urllib.parse import quote_plus + user = quote_plus(self.user) if self.user else "postgres" + cred = f"{user}:{quote_plus(self.password)}" if self.password else user + uri = f"postgresql://{cred}@{self.host}:{self.port}/{self.database}" + if self.schema: + uri += f"?options=-csearch_path%3D{quote_plus(self.schema)}" + return uri + + +class Pg0Config(BaseModel): + """pg0-embedded settings (local dev only, ignored when postgres.host is set).""" + name: str = "codespy" # MEMORY_PG0_NAME + port: int | None = None # MEMORY_PG0_PORT + data_dir: str | None = None # MEMORY_PG0_DATA_DIR + + class MemoryConfig(BaseModel): """Global memory (Hippocampus) configuration. @@ -65,11 +94,9 @@ class MemoryConfig(BaseModel): """ # PostgreSQL connection settings - postgres_uri: str | None = None # MEMORY_POSTGRES_URI (production) + postgres: PostgresConfig = Field(default_factory=PostgresConfig) + pg0: Pg0Config = Field(default_factory=Pg0Config) bank_id: str | None = None # MEMORY_BANK_ID (defaults to "codespy") - pg0_name: str = "codespy" # MEMORY_PG0_NAME (local dev) - pg0_port: int | None = None # MEMORY_PG0_PORT (auto-detected if unset) - pg0_data_dir: str | None = None # MEMORY_PG0_DATA_DIR (custom data directory for pg0) # Reflection defaults — overridable per-signature default_enabled: bool = False # MEMORY_DEFAULT_ENABLED @@ -119,16 +146,29 @@ class MemoryConfig(BaseModel): ) # MEMORY_CARTOGRAPHER_* +# Env var suffix (after MEMORY_POSTGRES_) -> PostgresConfig field name. +POSTGRES_ENV_SETTINGS = { + "HOST": "host", + "PORT": "port", + "USER": "user", + "PASSWORD": "password", + "DATABASE": "database", + "SCHEMA": "schema", +} + +# Env var suffix (after MEMORY_PG0_) -> Pg0Config field name. +PG0_ENV_SETTINGS = { + "NAME": "name", + "PORT": "port", + "DATA_DIR": "data_dir", +} + # 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 = { - "POSTGRES_URI": "postgres_uri", "BANK_ID": "bank_id", - "PG0_NAME": "pg0_name", - "PG0_PORT": "pg0_port", - "PG0_DATA_DIR": "pg0_data_dir", "DEFAULT_ENABLED": "default_enabled", "DEFAULT_MAX_REFLECTS": "default_max_reflects", "COMPACT_TRAJECTORY": "compact_trajectory", @@ -153,8 +193,14 @@ class MemoryConfig(BaseModel): 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} +# Maps env prefix (after MEMORY_) -> (config field name, suffix->field map) +NESTED_ENV_PREFIXES: dict[str, tuple[str, dict[str, str]]] = { + "POSTGRES_": ("postgres", POSTGRES_ENV_SETTINGS), + "PG0_": ("pg0", PG0_ENV_SETTINGS), +} +# Add reflection modules dynamically (same pattern, shared settings map) +for _mod in REFLECTION_MODULES: + NESTED_ENV_PREFIXES[f"{_mod.upper()}_"] = (_mod, REFLECTION_MODULE_ENV_SETTINGS) def _generate_bank_id() -> str: @@ -167,15 +213,18 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: Maps flat env vars onto the nested ``memory`` config, e.g.:: - MEMORY_POSTGRES_URI=postgresql://localhost/db -> memory.postgres_uri + MEMORY_POSTGRES_HOST=myhost -> memory.postgres.host MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled MEMORY_MAX_CONTEXT_MEMORY_TOKENS=512 -> memory.max_context_memory_tokens - Reflection module overrides use a second level of nesting:: + Nested sub-model overrides use a second level of nesting:: MEMORY_DISTILLER_MODEL=... -> memory.distiller.model MEMORY_CARTOGRAPHER_TEMPERATURE=0 -> memory.cartographer.temperature + MEMORY_PG0_NAME=mydb -> memory.pg0.name + MEMORY_PG0_PORT=5433 -> memory.pg0.port + Env vars take precedence over YAML, matching the documented priority (Environment Variables > YAML Config > Defaults). @@ -208,26 +257,26 @@ def apply_memory_env_overrides(config: dict[str, Any]) -> dict[str, Any]: 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 + # Nested sub-model: MEMORY_ + # Checked before the flat lookup, since e.g. MEMORY_PG0_NAME has no entry in # MEMORY_ENV_SETTINGS and would otherwise be silently dropped. - module_field = next( + nested = next( ( - (field, remainder[len(prefix) :]) - for prefix, field in REFLECTION_MODULE_PREFIXES.items() + (field, settings_map, remainder[len(prefix):]) + for prefix, (field, settings_map) in NESTED_ENV_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: + if nested is not None: + field, settings_map, setting = nested + setting_field = settings_map.get(setting) + if setting_field is None: continue - module_config = memory_config.setdefault(field, {}) - if not isinstance(module_config, dict): + sub_config = memory_config.setdefault(field, {}) + if not isinstance(sub_config, dict): continue - module_config[module_setting] = convert_env_value(value) + sub_config[setting_field] = convert_env_value(value) continue field = MEMORY_ENV_SETTINGS.get(remainder) @@ -254,7 +303,7 @@ def get_episode_store(settings: Settings) -> EpisodeStore | None: ``reload_settings``) to force a rebuild on next access. Priority: - 1. If ``memory.postgres_uri`` is set, use it directly. + 1. If ``memory.postgres.host`` is set, use the built URI to connect. 2. Else, try to auto-start pg0-embedded for local dev. 3. If pg0 is not available, return None with a warning. @@ -272,26 +321,27 @@ def get_episode_store(settings: Settings) -> EpisodeStore | None: bank_id = mem.bank_id or _generate_bank_id() # Try external PostgreSQL first - if mem.postgres_uri: + uri = mem.postgres.build_uri() + if uri: from codespy.agents.memory.postgres import EpisodeStore - _store = EpisodeStore(mem.postgres_uri, bank_id) + _store = EpisodeStore(uri, bank_id) logger.info(f"EpisodeStore connected to external PostgreSQL (bank={bank_id})") else: # Try pg0-embedded for local dev try: from codespy.agents.memory.pg0_manager import get_pg0_uri - uri = get_pg0_uri(name=mem.pg0_name, port=mem.pg0_port, data_dir=mem.pg0_data_dir) + uri = get_pg0_uri(name=mem.pg0.name, port=mem.pg0.port, data_dir=mem.pg0.data_dir) from codespy.agents.memory.postgres import EpisodeStore _store = EpisodeStore(uri, bank_id) logger.info(f"EpisodeStore connected to pg0-embedded PostgreSQL (bank={bank_id})") except ImportError: logger.warning( - "Memory is enabled but no PostgreSQL URI is configured and pg0-embedded " + "Memory is enabled but no PostgreSQL is configured and pg0-embedded " "is not installed. Install with: pip install pg0-embedded\n" - "Or set MEMORY_POSTGRES_URI to use an external PostgreSQL instance." + "Or set MEMORY_POSTGRES_HOST (+ credentials) to use an external PostgreSQL instance." ) _store = None except Exception as e: @@ -338,7 +388,7 @@ def verify_memory_access(settings: Settings) -> tuple[bool, str]: if store is None: return ( False, - "Memory is enabled but storage is not configured (set MEMORY_POSTGRES_URI or install pg0-embedded)", + "Memory is enabled but storage is not configured (set MEMORY_POSTGRES_HOST or install pg0-embedded)", ) try: diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py index 8d7d030..506a42a 100644 --- a/tests/test_config_memory.py +++ b/tests/test_config_memory.py @@ -5,6 +5,8 @@ import pytest from codespy.config_memory import ( + PostgresConfig, + Pg0Config, _generate_bank_id, apply_memory_env_overrides, get_episode_store, @@ -25,12 +27,47 @@ def test_generate_bank_id_returns_codespy(self): class TestApplyMemoryEnvOverrides: """Tests for apply_memory_env_overrides function.""" - def test_override_postgres_uri(self, monkeypatch): - """MEMORY_POSTGRES_URI should set memory.postgres_uri.""" - monkeypatch.setenv("MEMORY_POSTGRES_URI", "postgresql://localhost:5432/test") + def test_override_postgres_host(self, monkeypatch): + """MEMORY_POSTGRES_HOST should set memory.postgres.host.""" + monkeypatch.setenv("MEMORY_POSTGRES_HOST", "myhost.example.com") config = {} result = apply_memory_env_overrides(config) - assert result["memory"]["postgres_uri"] == "postgresql://localhost:5432/test" + assert result["memory"]["postgres"]["host"] == "myhost.example.com" + + def test_override_postgres_port(self, monkeypatch): + """MEMORY_POSTGRES_PORT should set memory.postgres.port as int.""" + monkeypatch.setenv("MEMORY_POSTGRES_PORT", "5433") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres"]["port"] == 5433 + + def test_override_postgres_user(self, monkeypatch): + """MEMORY_POSTGRES_USER should set memory.postgres.user.""" + monkeypatch.setenv("MEMORY_POSTGRES_USER", "admin") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres"]["user"] == "admin" + + def test_override_postgres_password(self, monkeypatch): + """MEMORY_POSTGRES_PASSWORD should set memory.postgres.password.""" + monkeypatch.setenv("MEMORY_POSTGRES_PASSWORD", "secret123") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres"]["password"] == "secret123" + + def test_override_postgres_database(self, monkeypatch): + """MEMORY_POSTGRES_DATABASE should set memory.postgres.database.""" + monkeypatch.setenv("MEMORY_POSTGRES_DATABASE", "mydb") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres"]["database"] == "mydb" + + def test_override_postgres_schema(self, monkeypatch): + """MEMORY_POSTGRES_SCHEMA should set memory.postgres.schema.""" + monkeypatch.setenv("MEMORY_POSTGRES_SCHEMA", "public") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["postgres"]["schema"] == "public" def test_override_bank_id(self, monkeypatch): """MEMORY_BANK_ID should set memory.bank_id.""" @@ -40,18 +77,25 @@ def test_override_bank_id(self, monkeypatch): assert result["memory"]["bank_id"] == "my-agent" def test_override_pg0_name(self, monkeypatch): - """MEMORY_PG0_NAME should set memory.pg0_name.""" + """MEMORY_PG0_NAME should set memory.pg0.name.""" monkeypatch.setenv("MEMORY_PG0_NAME", "custom_name") config = {} result = apply_memory_env_overrides(config) - assert result["memory"]["pg0_name"] == "custom_name" + assert result["memory"]["pg0"]["name"] == "custom_name" def test_override_pg0_port(self, monkeypatch): - """MEMORY_PG0_PORT should set memory.pg0_port as int.""" + """MEMORY_PG0_PORT should set memory.pg0.port as int.""" monkeypatch.setenv("MEMORY_PG0_PORT", "5433") config = {} result = apply_memory_env_overrides(config) - assert result["memory"]["pg0_port"] == 5433 + assert result["memory"]["pg0"]["port"] == 5433 + + def test_override_pg0_data_dir(self, monkeypatch): + """MEMORY_PG0_DATA_DIR should set memory.pg0.data_dir.""" + monkeypatch.setenv("MEMORY_PG0_DATA_DIR", "/custom/path") + config = {} + result = apply_memory_env_overrides(config) + assert result["memory"]["pg0"]["data_dir"] == "/custom/path" def test_override_default_enabled(self, monkeypatch): """MEMORY_DEFAULT_ENABLED should set memory.default_enabled as bool.""" @@ -69,22 +113,100 @@ def test_override_reflection_module(self, monkeypatch): def test_no_memory_prefix_ignored(self, monkeypatch): """Non-MEMORY_ env vars should be ignored.""" - monkeypatch.setenv("OTHER_POSTGRES_URI", "postgresql://localhost/db") + monkeypatch.setenv("OTHER_POSTGRES_HOST", "myhost.example.com") config = {} result = apply_memory_env_overrides(config) - assert "memory" not in result or "postgres_uri" not in result.get("memory", {}) + assert "memory" not in result or "postgres" not in result.get("memory", {}) + + +class TestPostgresConfigBuildUri: + """Tests for PostgresConfig.build_uri() method.""" + + def test_build_uri_returns_none_when_no_host(self): + """Should return None when host is unset.""" + config = PostgresConfig() + assert config.build_uri() is None + + def test_build_uri_basic(self): + """Should build basic URI with all fields.""" + config = PostgresConfig( + host="db.example.com", + user="u", + password="p", + database="codespy" + ) + result = config.build_uri() + assert result == "postgresql://u:p@db.example.com:5432/codespy" + + def test_build_uri_no_password(self): + """Should not include password segment when password is None.""" + config = PostgresConfig( + host="db.example.com", + user="u" + ) + result = config.build_uri() + assert result == "postgresql://u@db.example.com:5432/codespy" + + def test_build_uri_no_user_no_password(self): + """Should default to 'postgres' user when user is None.""" + config = PostgresConfig(host="db.example.com") + result = config.build_uri() + assert result == "postgresql://postgres@db.example.com:5432/codespy" + + def test_build_uri_special_chars_in_password(self): + """Should URL-encode special characters in password.""" + config = PostgresConfig( + host="db.example.com", + user="u", + password="p@ss:w/rd" + ) + result = config.build_uri() + assert "p%40ss%3Aw%2Frd" in result + assert result == "postgresql://u:p%40ss%3Aw%2Frd@db.example.com:5432/codespy" + + def test_build_uri_with_schema(self): + """Should append search_path option when schema is set.""" + config = PostgresConfig( + host="db.example.com", + schema="my_schema" + ) + result = config.build_uri() + assert result == "postgresql://postgres@db.example.com:5432/codespy?options=-csearch_path%3Dmy_schema" + + def test_build_uri_custom_port_and_database(self): + """Should use custom port and database.""" + config = PostgresConfig( + host="db.example.com", + port=5433, + database="mydb" + ) + result = config.build_uri() + assert result == "postgresql://postgres@db.example.com:5433/mydb" + + def test_build_uri_empty_password_treated_as_none(self): + """Empty string password should be treated as no password.""" + config = PostgresConfig( + host="db.example.com", + user="u", + password="" + ) + result = config.build_uri() + assert result == "postgresql://u@db.example.com:5432/codespy" class TestGetEpisodeStore: """Tests for get_episode_store function.""" def test_returns_none_when_no_config(self): - """Should return None when postgres_uri not set and pg0 not available.""" + """Should return None when postgres.host not set and pg0 not available.""" settings = MagicMock() - settings.memory.postgres_uri = None + settings.memory.postgres = MagicMock() + settings.memory.postgres.build_uri.return_value = None + settings.memory.pg0 = MagicMock() + settings.memory.pg0.name = "codespy" + settings.memory.pg0.port = None + settings.memory.pg0.data_dir = None settings.memory.bank_id = "test-bank" - settings.memory.pg0_name = "codespy" - settings.memory.pg0_port = None # Simulate pg0 not being available with patch("codespy.config_memory._store", None): @@ -98,9 +220,10 @@ def test_returns_none_when_no_config(self): assert result is None def test_uses_postgres_uri_when_set(self): - """Should use external PostgreSQL when MEMORY_POSTGRES_URI is set.""" + """Should use external PostgreSQL when postgres.build_uri() returns a URI.""" settings = MagicMock() - settings.memory.postgres_uri = "postgresql://localhost:5432/codespy" + settings.memory.postgres = MagicMock() + settings.memory.postgres.build_uri.return_value = "postgresql://u:p@host:5432/codespy" settings.memory.bank_id = "test-bank" mock_store = MagicMock() @@ -114,14 +237,15 @@ def test_uses_postgres_uri_when_set(self): result = get_episode_store(settings) mock_episode_store.assert_called_once_with( - "postgresql://localhost:5432/codespy", "test-bank" + "postgresql://u:p@host:5432/codespy", "test-bank" ) assert result == mock_store def test_caching_behavior(self): """Should cache the store after first call.""" settings = MagicMock() - settings.memory.postgres_uri = "postgresql://localhost:5432/codespy" + settings.memory.postgres = MagicMock() + settings.memory.postgres.build_uri.return_value = "postgresql://localhost:5432/codespy" settings.memory.bank_id = "test-bank" mock_store = MagicMock() From 490787b9cb1118f1b2cc676e508d5010cbc7b086 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 11 Sep 2026 11:16:39 +0200 Subject: [PATCH 11/14] schemas --- .env.example | 2 +- action.yml | 3 ++- codespy.yaml | 2 +- docs/configuration.md | 2 +- docs/memory.md | 2 +- src/codespy/agents/memory/postgres.py | 29 +++++++++++++++++++++++---- src/codespy/config_memory.py | 24 +++++++++++++--------- tests/test_config_memory.py | 12 +++++++---- 8 files changed, 53 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 6cb0e32..6a98f25 100644 --- a/.env.example +++ b/.env.example @@ -199,7 +199,7 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # MEMORY_POSTGRES_USER=myuser # MEMORY_POSTGRES_PASSWORD=mypassword # MEMORY_POSTGRES_DATABASE=codespy -# MEMORY_POSTGRES_SCHEMA=public # PostgreSQL search_path (optional) +# MEMORY_POSTGRES_SCHEMA=episodic # PostgreSQL search_path (default: episodic; each memory type uses its own) # Bank ID — nickname, username, email, or agent name scoping all memory data. # When unset, defaults to "codespy". diff --git a/action.yml b/action.yml index 5cae683..1d684a8 100644 --- a/action.yml +++ b/action.yml @@ -332,8 +332,9 @@ inputs: required: false default: 'codespy' memory-postgres-schema: - description: 'PostgreSQL schema / search_path (optional)' + description: 'PostgreSQL schema / search_path (default: episodic)' required: false + default: 'episodic' memory-bank-id: description: 'Bank ID scoping all memory data (e.g., project name or agent name). Defaults to "codespy".' diff --git a/codespy.yaml b/codespy.yaml index fd5befe..d355beb 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -86,7 +86,7 @@ memory: user: null # MEMORY_POSTGRES_USER (default: postgres) password: null # MEMORY_POSTGRES_PASSWORD database: codespy # MEMORY_POSTGRES_DATABASE - schema: null # MEMORY_POSTGRES_SCHEMA (search_path; null = server default) + schema: episodic # MEMORY_POSTGRES_SCHEMA (search_path per memory type; null = public) # pg0-embedded settings (local dev only, ignored when postgres.host is set) pg0: diff --git a/docs/configuration.md b/docs/configuration.md index 6314e2a..b45b70c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -177,7 +177,7 @@ Brief overview: | PostgreSQL user | `MEMORY_POSTGRES_USER` | `postgres` | External PostgreSQL user | | PostgreSQL password | `MEMORY_POSTGRES_PASSWORD` | — | External PostgreSQL password | | PostgreSQL database | `MEMORY_POSTGRES_DATABASE` | `codespy` | External PostgreSQL database | -| PostgreSQL schema | `MEMORY_POSTGRES_SCHEMA` | — | PostgreSQL search_path (optional) | +| PostgreSQL schema | `MEMORY_POSTGRES_SCHEMA` | `episodic` | PostgreSQL schema / search_path per memory type | | Bank ID | `MEMORY_BANK_ID` | `codespy` | Scopes all memory data | | pg0 name | `MEMORY_PG0_NAME` | `codespy` | pg0-embedded database name (local dev) | | pg0 port | `MEMORY_PG0_PORT` | auto | pg0-embedded port (local dev) | diff --git a/docs/memory.md b/docs/memory.md index 3e4060e..92b43b5 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -209,7 +209,7 @@ Observation capacity ≈ context_memory_tokens / observation_tokens (16384/512 = | `MEMORY_POSTGRES_USER` | `memory.postgres.user` | `postgres` | External PostgreSQL user | | `MEMORY_POSTGRES_PASSWORD` | `memory.postgres.password` | — | External PostgreSQL password | | `MEMORY_POSTGRES_DATABASE` | `memory.postgres.database` | `codespy` | External PostgreSQL database | -| `MEMORY_POSTGRES_SCHEMA` | `memory.postgres.schema` | — | PostgreSQL search_path | +| `MEMORY_POSTGRES_SCHEMA` | `memory.postgres.schema` | `episodic` | PostgreSQL schema (search_path per memory type) | | `MEMORY_PG0_NAME` | `memory.pg0.name` | `codespy` | pg0-embedded database name | | `MEMORY_PG0_PORT` | `memory.pg0.port` | auto | pg0-embedded port | | `MEMORY_PG0_DATA_DIR` | `memory.pg0.data_dir` | — | Custom data directory for pg0-embedded | diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index 79f3c2d..d0e6d38 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -34,18 +34,30 @@ def __init__( self, conninfo: str, bank_id: str, + schema: str | None = None, min_size: int = 1, max_size: int = 4, ): """Create store with a psycopg ConnectionPool, scoped to a bank. Args: - conninfo: PostgreSQL connection string (e.g., postgresql://localhost:5432/dbname) - bank_id: Identifier for the bank (nickname, username, email, agent name) + conninfo: PostgreSQL connection string + bank_id: Identifier for the bank + schema: PostgreSQL schema name. When set, CREATE SCHEMA IF NOT + EXISTS is run and search_path is set for every pooled + connection. Each memory type uses its own schema + (e.g. "episodic", "semantic"). None = server default (public). min_size: Minimum connections in pool max_size: Maximum connections in pool """ self.bank_id = bank_id + self._schema = schema + # Inject search_path into the connection string so every pooled + # connection targets the right schema automatically. + if schema: + from urllib.parse import quote_plus + sep = "&" if "?" in conninfo else "?" + conninfo = f"{conninfo}{sep}options=-csearch_path%3D{quote_plus(schema)}%2Cpublic" self._pool = ConnectionPool( conninfo=conninfo, min_size=min_size, @@ -62,8 +74,17 @@ def ensure_schema(self) -> None: """Auto-create tables if not present (idempotent).""" with self._pool.connection() as conn: with conn.cursor() as cur: - # Extensions - cur.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm") + # Create the target schema if it doesn't exist yet + if self._schema: + cur.execute( + sql.SQL("CREATE SCHEMA IF NOT EXISTS {}").format( + sql.Identifier(self._schema) + ) + ) + + # Extensions — explicitly in public so they're accessible + # from any schema's search_path (episodic, semantic, etc.) + cur.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA public") # Schema version table cur.execute(""" diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 63e5fd1..9414cf3 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -63,19 +63,22 @@ class PostgresConfig(BaseModel): user: str | None = None # MEMORY_POSTGRES_USER password: str | None = None # MEMORY_POSTGRES_PASSWORD database: str = "codespy" # MEMORY_POSTGRES_DATABASE - schema: str | None = None # MEMORY_POSTGRES_SCHEMA (search_path) + schema: str | None = "episodic" # MEMORY_POSTGRES_SCHEMA (search_path per memory type; None = public) def build_uri(self) -> str | None: - """Build a psycopg connection URI. Returns None when host is unset.""" + """Build a psycopg connection URI. Returns None when host is unset. + + Schema is NOT included in the URI. Each memory store (EpisodeStore, + future SemanticStore) handles CREATE SCHEMA and SET search_path + itself, so multiple stores can share the same base URI while + targeting different schemas. + """ if not self.host: return None from urllib.parse import quote_plus user = quote_plus(self.user) if self.user else "postgres" cred = f"{user}:{quote_plus(self.password)}" if self.password else user - uri = f"postgresql://{cred}@{self.host}:{self.port}/{self.database}" - if self.schema: - uri += f"?options=-csearch_path%3D{quote_plus(self.schema)}" - return uri + return f"postgresql://{cred}@{self.host}:{self.port}/{self.database}" class Pg0Config(BaseModel): @@ -319,14 +322,15 @@ def get_episode_store(settings: Settings) -> EpisodeStore | None: mem = settings.memory bank_id = mem.bank_id or _generate_bank_id() + schema = mem.postgres.schema # "episodic" by default # Try external PostgreSQL first uri = mem.postgres.build_uri() if uri: from codespy.agents.memory.postgres import EpisodeStore - _store = EpisodeStore(uri, bank_id) - logger.info(f"EpisodeStore connected to external PostgreSQL (bank={bank_id})") + _store = EpisodeStore(uri, bank_id, schema=schema) + logger.info(f"EpisodeStore connected to external PostgreSQL (bank={bank_id}, schema={schema})") else: # Try pg0-embedded for local dev try: @@ -335,8 +339,8 @@ def get_episode_store(settings: Settings) -> EpisodeStore | None: uri = get_pg0_uri(name=mem.pg0.name, port=mem.pg0.port, data_dir=mem.pg0.data_dir) from codespy.agents.memory.postgres import EpisodeStore - _store = EpisodeStore(uri, bank_id) - logger.info(f"EpisodeStore connected to pg0-embedded PostgreSQL (bank={bank_id})") + _store = EpisodeStore(uri, bank_id, schema=schema) + logger.info(f"EpisodeStore connected to pg0-embedded PostgreSQL (bank={bank_id}, schema={schema})") except ImportError: logger.warning( "Memory is enabled but no PostgreSQL is configured and pg0-embedded " diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py index 506a42a..dbbf27c 100644 --- a/tests/test_config_memory.py +++ b/tests/test_config_memory.py @@ -164,14 +164,16 @@ def test_build_uri_special_chars_in_password(self): assert "p%40ss%3Aw%2Frd" in result assert result == "postgresql://u:p%40ss%3Aw%2Frd@db.example.com:5432/codespy" - def test_build_uri_with_schema(self): - """Should append search_path option when schema is set.""" + def test_build_uri_does_not_include_schema(self): + """Schema is handled by each store, not in the URI.""" config = PostgresConfig( host="db.example.com", schema="my_schema" ) result = config.build_uri() - assert result == "postgresql://postgres@db.example.com:5432/codespy?options=-csearch_path%3Dmy_schema" + assert "search_path" not in result + assert "options" not in result + assert result == "postgresql://postgres@db.example.com:5432/codespy" def test_build_uri_custom_port_and_database(self): """Should use custom port and database.""" @@ -202,6 +204,7 @@ def test_returns_none_when_no_config(self): settings = MagicMock() settings.memory.postgres = MagicMock() settings.memory.postgres.build_uri.return_value = None + settings.memory.postgres.schema = "episodic" settings.memory.pg0 = MagicMock() settings.memory.pg0.name = "codespy" settings.memory.pg0.port = None @@ -224,6 +227,7 @@ def test_uses_postgres_uri_when_set(self): settings = MagicMock() settings.memory.postgres = MagicMock() settings.memory.postgres.build_uri.return_value = "postgresql://u:p@host:5432/codespy" + settings.memory.postgres.schema = "episodic" settings.memory.bank_id = "test-bank" mock_store = MagicMock() @@ -237,7 +241,7 @@ def test_uses_postgres_uri_when_set(self): result = get_episode_store(settings) mock_episode_store.assert_called_once_with( - "postgresql://u:p@host:5432/codespy", "test-bank" + "postgresql://u:p@host:5432/codespy", "test-bank", schema="episodic" ) assert result == mock_store From 632cadf6537bd9ca5fd4c24ac80a301df3afcd4f Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 11 Sep 2026 12:50:15 +0200 Subject: [PATCH 12/14] doc --- docs/configuration.md | 1 + docs/development.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index b45b70c..3745d30 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -181,6 +181,7 @@ Brief overview: | Bank ID | `MEMORY_BANK_ID` | `codespy` | Scopes all memory data | | pg0 name | `MEMORY_PG0_NAME` | `codespy` | pg0-embedded database name (local dev) | | pg0 port | `MEMORY_PG0_PORT` | auto | pg0-embedded port (local dev) | +| pg0 data dir | `MEMORY_PG0_DATA_DIR` | — | pg0-embedded data directory (local dev) | | 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_MAX_CONTEXT_MEMORY_TOKENS` | `16384` | Ceiling on persisted context memory | diff --git a/docs/development.md b/docs/development.md index 14b0c20..704ccb3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -61,7 +61,9 @@ src/codespy/ │ ├── cost_tracker.py # Token/cost tracking │ ├── dspy_config.py # DSPy runtime config │ ├── memory/ # Hippocampus memory system -│ │ └── hippocampus/ # Episode persistence, context memory, budget +│ │ ├── hippocampus/ # Episode persistence, context memory, budget +│ │ ├── pg0_manager.py # pg0-embedded PostgreSQL lifecycle +│ │ └── postgres.py # PostgreSQL episode persistence │ └── reviewer/ # Review pipeline │ ├── models.py # Review data models │ ├── reviewer.py # Main review orchestrator From d3987e3dcdeafd93e63df6956b80755ef5722334 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 11 Sep 2026 13:08:46 +0200 Subject: [PATCH 13/14] sql inject --- src/codespy/agents/memory/postgres.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index d0e6d38..c315002 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -546,26 +546,26 @@ def load_context( conn.row_factory = dict_row with conn.cursor() as cur: # 1. Find the latest episode for this task + topics - topic_filter = "" params: list = [self.bank_id, task] if topic_ids: - topic_filter = "AND et.topic_id = ANY(%s)" + filter_clause = sql.SQL("AND et.topic_id = ANY(%s)") params.append(topic_ids) elif topic_prefix: - topic_filter = "AND et.topic_id LIKE %s" + filter_clause = sql.SQL("AND et.topic_id LIKE %s") params.append(f"{topic_prefix}%") + else: + filter_clause = sql.SQL("") - cur.execute( - f""" + query = sql.SQL(""" SELECT e.id FROM episodes e JOIN episode_topics et ON et.bank_id = e.bank_id AND et.episode_id = e.id - WHERE e.bank_id = %s AND e.task = %s {topic_filter} + WHERE e.bank_id = %s AND e.task = %s {} ORDER BY e.timestamp DESC LIMIT 1 - """, - params, - ) + """).format(filter_clause) + + cur.execute(query, params) row = cur.fetchone() if not row: # Diagnostic: how many episodes exist for this bank+task (ignoring topic filter)? From b04cc1a3b50682fdec9c59272aa5fb2de3c51c94 Mon Sep 17 00:00:00 2001 From: Guillaume Simonneau Date: Fri, 11 Sep 2026 19:07:11 +0200 Subject: [PATCH 14/14] changelog + version --- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/codespy/__init__.py | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b612e..226f988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,54 @@ ## [Unreleased] +## [1.2.0] - 2026-09-11 + +### Changed +- **BREAKING**: Memory storage backend migrated from filesystem/S3 to PostgreSQL + - `MemoryConfig` fields removed: `backend`, `root`, `s3_bucket`, `s3_region`, `s3_endpoint_url` + - New `PostgresConfig` sub-model (`memory.postgres.*`): `host`, `port`, `user`, `password`, `database`, `schema` + - New `Pg0Config` sub-model (`memory.pg0.*`): `name`, `port`, `data_dir` (local dev via pg0-embedded) + - New `bank_id` field on `MemoryConfig` scoping all memory data + - Env vars removed: `MEMORY_BACKEND`, `MEMORY_ROOT`, `MEMORY_S3_BUCKET`, `MEMORY_S3_REGION`, `MEMORY_S3_ENDPOINT_URL` + - Env vars added: `MEMORY_POSTGRES_HOST`, `MEMORY_POSTGRES_PORT`, `MEMORY_POSTGRES_USER`, `MEMORY_POSTGRES_PASSWORD`, `MEMORY_POSTGRES_DATABASE`, `MEMORY_POSTGRES_SCHEMA`, `MEMORY_PG0_NAME`, `MEMORY_PG0_PORT`, `MEMORY_PG0_DATA_DIR`, `MEMORY_BANK_ID` +- **BREAKING**: `Item` renamed to `Observation` throughout context memory + - `ItemTag` → `ObservationTag` + - `Operation.item_id` → `Operation.observation_id` + - `Mutation.item_id` → `Mutation.observation_id` + - `ContextMemory.all_items()` → `ContextMemory.all_observations()` + - `ContextMemory.find_item()` → `ContextMemory.find_observation()` + - `Hippocampus._update_item_scores()` → `Hippocampus._update_observation_scores()` +- **BREAKING**: `Topic` model gains required `type` field (e.g. `"project_scope"`, `"pull_request"`) +- **BREAKING**: `get_memory_store()` / `reset_memory_store()` renamed to `get_episode_store()` / `reset_episode_store()` +- **BREAKING**: `Hippocampus.end_episode()` signature: `store` type `Storage` → `EpisodeStore`; `dir` parameter removed +- **BREAKING**: `Hippocampus.save_episode()`, `Hippocampus.load_episode()` removed; use `EpisodeStore.save_episode()` directly +- **BREAKING**: `find_latest_episode()`, `save_episode()`, `load_episode()` removed from `episode.py`; replaced by `EpisodeStore.load_context()` +- **BREAKING**: `ContextMemory.merge()`, `.to_json()`, `.from_json()` removed; `EpisodeStore.load_context()` returns a merged view via SQL +- **BREAKING**: GitHub Action inputs replaced: `memory-backend`, `memory-root`, `memory-s3-*` → `memory-postgres-*`, `memory-bank-id` +- All review modules (code_reviewer, doc_reviewer, supply_chain_auditor, auditor, summarizer, scope_resolver) now use `EpisodeStore.load_context()` with topic-based queries instead of `find_latest_episode()` with filesystem path patterns +- `PRContext.repo_full_name` property added, stripping host prefix from `repo_slug` +- REPLACE on non-existent observation with valid section prefix now falls back to ADD instead of silently skipping +- REPLACE with topic-ID-shaped `observation_id` (no section prefix) logs warning and skips +- `Hippocampus._record_mutations()` handles REPLACE-to-ADD fallback to keep mutations aligned with `new_ids` +- `Episode.id` field added (caller-provided UUID); `Episode.timestamp` no longer auto-generated +- `reset_episode_store()` calls `store.close()` before clearing the cache +- SQL in `postgres.py` refactored from f-strings to `psycopg.sql.SQL().format()` + +### Added +- `src/codespy/agents/memory/postgres.py` — `EpisodeStore`: relational episode storage with schema (banks, episodes, topics, observations, observation_topics, episode_topics, mutations) +- `src/codespy/agents/memory/pg0_manager.py` — pg0-embedded lifecycle (`get_pg0_uri()`, `stop_pg0()`) for zero-config local dev +- `psycopg` dependency (>=3.1, extras: binary + pool) +- `pg0-embedded` dependency (>=0.15) +- `libgssapi-krb5-2` in Dockerfile for Kerberos/GSSAPI PostgreSQL auth +- `_PREFIX_TO_SECTION` reverse mapping in `context_memory.py` + +### Removed +- `ContextMemory.merge()`, `.to_json()`, `.from_json()` +- `find_latest_episode()`, `save_episode()`, `load_episode()` from `episode.py` +- `Hippocampus.save_episode()`, `.load_episode()`, `.episode_file_path()`, `_episode_index` +- `MemoryBackend` type alias +- Storage dependency (`codespy.tools.storage.base.Storage`, `codespy.tools.storage.models`) in hippocampus/episode modules + ## [1.0.16] - 2026-09-01 ### Changed diff --git a/pyproject.toml b/pyproject.toml index 5d6d9df..ed9cc99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.0.16" +version = "1.2.0" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index 7c3e0be..0b230e1 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.0.16" +__version__ = "1.2.0"