diff --git a/.env.example b/.env.example index 9dab44b..1f46c6b 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,11 @@ # NOTE: For advanced configuration (per-signature settings, model overrides), # use codespy.yaml instead. See codespy.yaml for full options. # Priority: Environment Variables > YAML Config > Defaults +# +# Environment variables follow the YAML path, uppercased and joined with _. +# The llm., memory.hippocampus., memory.cerebral. and memory.prefrontal. +# segments are dropped; llm.retries, llm.timeout and memory.prefrontal.reflects +# keep their full names. # ============================================================================= # Git Platform Tokens @@ -42,38 +47,38 @@ GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # - gemini/gemini-2.5-pro (Google Gemini) # - gemini/gemini-2.5-flash (Google Gemini) # - ollama/llama-4-70b (Local Ollama) -DEFAULT_MODEL=anthropic/claude-opus-4-6 +DEFAULT_MODEL=bedrock/converse/nvidia.nemotron-nano-3-30b -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Option 1: Anthropic (direct) -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Auto-discovered from: $ANTHROPIC_API_KEY, ~/.config/anthropic/, ~/.anthropic/ # ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Option 2: AWS Bedrock -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Auto-discovered from: ~/.aws/credentials, AWS CLI, $AWS_ACCESS_KEY_ID # AWS_REGION=us-east-1 # AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE # AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # AWS_PROFILE=default -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Option 3: OpenAI -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Auto-discovered from: $OPENAI_API_KEY, ~/.config/openai/, ~/.openai/ # OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Option 4: Google Gemini -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Auto-discovered from: $GEMINI_API_KEY, $GOOGLE_API_KEY, gcloud ADC # GEMINI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Option 5: Azure OpenAI -# ----------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # AZURE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # AZURE_API_BASE=https://your-resource.openai.azure.com/ # AZURE_API_VERSION=2024-02-15-preview @@ -92,27 +97,27 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # ============================================================================= # codespy uses a tiered model approach to balance review quality and cost: # -# Smart (DEFAULT_MODEL): Core analysis — code review (bugs, security, smells), +# Smart (per-signature pinned): Core analysis — code review (bugs, security, smells), # supply chain, scope identification, doc review. Requires strong reasoning. -# Recommended: anthropic/claude-opus-4-6 or equivalent. +# Default: bedrock/converse/global.anthropic.claude-opus-5-5 (via per-signature pins). # # Mid-tier (EXTRACTION_MODEL): Structured field extraction. Needs accuracy -# but not deep reasoning. Recommended: anthropic/claude-sonnet-4-5-20250929 -# or equivalent. +# but not deep reasoning. Default: bedrock/converse/nvidia.nemotron-nano-3-30b # -# Cheap (SUMMARY_MODEL): PR summary generation. Simple synthesis. -# Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. +# Cheap (REVIEW_SUMMARY_MODEL): PR summary generation. Simple synthesis. +# Default: bedrock/converse/nvidia.nemotron-super-3-120b # -# Mid-tier (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): Memory -# reflection — summarizing a trajectory and curating the context memory. +# Mid-tier (MEMORY_DISTILLER_MODEL / MEMORY_CARTOGRAPHER_MODEL): +# Memory reflection — summarizing a trajectory and curating the context memory. # Requires nuance. -# Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. +# Default: bedrock/converse/nvidia.nemotron-super-3-120b +# +# By default, all models are pinned. To override a specific unit, set its +# per-signature or per-memory env var. To re-enable the fallback chain, set the +# pinned unit to null: REVIEW_CODE_REVIEW_MODEL=null uses DEFAULT_MODEL. # -# By default, all models fall back to DEFAULT_MODEL. To optimize costs: -# EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 -# SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 -# MEMORY_DISTILLER_MODEL=anthropic/claude-sonnet-4-5-20250929 -# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 +# New defaults require AWS Bedrock access to Opus 5.5 and both Nemotron models. +# Override every model if using Anthropic-only, OpenAI-only, or Gemini-only. # ============================================================================= @@ -120,138 +125,83 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # ============================================================================= # These apply to all signatures unless overridden per-signature -# Extraction model for TwoStepAdapter (extracts structured fields from free-form LLM responses) -# Falls back to DEFAULT_MODEL if not set -# EXTRACTION_MODEL=anthropic/claude-sonnet-4-5-20250929 +# Extraction model for TwoStepAdapter +# EXTRACTION_MODEL=bedrock/converse/nvidia.nemotron-nano-3-30b # DEFAULT_MAX_ITERS=5 # DEFAULT_MAX_LLM_CALLS=8 # Provider reasoning budget: minimal | low | medium | high # DEFAULT_REASONING_EFFORT=medium -# DEFAULT_TEMPERATURE=0.2 - -# Output token budget for a single completion (default: 32000). This is an OUTPUT -# ceiling, not a context window, and reasoning/thinking tokens are charged against -# it — so keep it well above the expected answer size while DEFAULT_REASONING_EFFORT -# is set. codespy always sends an explicit value: without one LiteLLM silently falls -# back to its own 4096 default and truncates responses mid-answer (DSPy warns -# "LM response was truncated ... max_tokens=None"). -# The value is clamped down to each model's real output ceiling before use, so a -# generous setting stays valid on smaller models; models LiteLLM doesn't know -# (Ollama, custom endpoints) receive it unchanged. -# NOTE: LiteLLM also reads DEFAULT_MAX_TOKENS for its own internal fallback, so -# setting this aligns both layers. +# DEFAULT_TEMPERATURE=1.0 +# Output token budget for a single completion (default: 32000) # DEFAULT_MAX_TOKENS=32000 # Global LLM reliability settings -# Number of retries for LLM API calls # LLM_RETRIES=2 -# Timeout in seconds for LLM calls # LLM_TIMEOUT=240 -# Enable provider-side prompt caching (reduces latency and costs) -# ENABLE_PROMPT_CACHING=true - -# 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) # ============================================================================= -# Proactive fallback to RLM when input tokens exceed a ratio of the model's -# max_input_tokens. Prevents quality degradation ("context rot") that occurs -# well before the hard context window limit. The hard overflow safety net -# remains active regardless of these settings. -# -# Thresholds are per module type: -# react: Multi-step tool-using agents (code_review, scope, supply_chain) -# chain_of_thought: Single-pass reasoning (doc, summary, audit) -# predict: Basic completion (not currently used) -# -# Minimum floor: proactive threshold never fires below 8192 input tokens. - # RLM_FALLBACK_ENABLED=true # RLM_FALLBACK_REACT_THRESHOLD=0.30 # RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD=0.40 # RLM_FALLBACK_PREDICT_THRESHOLD=0.50 +# ============================================================================= +# Minimum Confidence +# ============================================================================= +# REVIEW_MIN_CONFIDENCE=0.81 + + # ============================================================================= # Memory (Hippocampus) # ============================================================================= -# Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# consolidate their run into a ContextMemory and persist it as an Episode. -# Episodes are stored in PostgreSQL using a relational schema with pgvector -# for semantic search. +# Hippocampus lets scope-based agents consolidate their run into a ContextMemory +# and persist it as an Episode. Episodes are stored in PostgreSQL. # # 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. +# PostgreSQL connection # 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=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". +# When unset, defaults to "codebase". # MEMORY_BANK_ID=my-agent -# pg0 embedded settings (local dev only, ignored when MEMORY_POSTGRES_HOST is set) +# pg0 embedded settings (local dev only) # MEMORY_PG0_NAME=codespy -# MEMORY_PG0_PORT=5432 -# MEMORY_PG0_DATA_DIR=~/.cache/codespy/pg0 # default; persists on Docker-mounted codespy-cache volume +# MEMORY_PG0_PORT=5433 +# MEMORY_PG0_DATA_DIR=~/.cache/codespy/pg0 -# Memory default — overridable per-signature via _MEMORY_* -# MEMORY_DEFAULT_ENABLED=false +# Memory master switch +# MEMORY_ENABLED=false # Whether to compact trajectories (head+tail bounding) before distillation. -# When false, the full trajectory is sent to the Distiller and ContextSafe -# RLM fallback handles overflow if the trajectory exceeds the model's context. # MEMORY_COMPACT_TRAJECTORY=true -# Four independent token budgets, most to least cost-sensitive: -# -# 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 / 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=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 -# (unbounded — not recommended for tool-using agents). +# Token budgets +# MEMORY_MAX_HIPPOCAMPUS_TOKENS=16384 +# MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS=512 # MEMORY_MAX_TRAJECTORY_TOKENS=16384 -# 4. Cap on the serialized agent inputs used as the reflection "question". Without -# it, every input field is sent in full (for code review, the complete patch of -# every changed file). Unset = unbounded. # MEMORY_MAX_QUESTION_TOKENS=8192 -# LLM settings for the reflection modules. The Distiller summarizes a trajectory; -# the Cartographer curates the context memory. Both are compact summarize/curate -# tasks, so a cheaper tier than code review usually suffices. -# Each falls back to the corresponding DEFAULT_* value when unset. -# MEMORY_DISTILLER_MODEL=anthropic/claude-haiku-4-5-20251001 +# LLM settings for reflection modules +# MEMORY_DISTILLER_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_DISTILLER_REASONING_EFFORT=low # MEMORY_DISTILLER_TEMPERATURE=1 # MEMORY_DISTILLER_MAX_TOKENS=64000 # MEMORY_DISTILLER_MAX_ITERS=1 # MEMORY_DISTILLER_MAX_LLM_CALLS=2 -# MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-haiku-4-5-20251001 +# MEMORY_CARTOGRAPHER_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_CARTOGRAPHER_REASONING_EFFORT=low # MEMORY_CARTOGRAPHER_TEMPERATURE=1 # MEMORY_CARTOGRAPHER_MAX_TOKENS=64000 @@ -259,76 +209,123 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6 # MEMORY_CARTOGRAPHER_MAX_LLM_CALLS=2 +# ============================================================================= +# Cerebral (Semantic Memory) +# ============================================================================= +# Cerebral activates automatically when MEMORY_ENABLED=true. +# LLM provider and credentials are derived from the model string and existing +# LLM config (e.g. bedrock uses AWS env vars, openai uses OPENAI_API_KEY). +# MEMORY_RETAIN_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b +# MEMORY_CONSOLIDATION_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b +# MEMORY_MENTAL_MODELS_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b +# MEMORY_EMBEDDINGS_MODEL=bedrock/cohere.embed-v4:0 +# MEMORY_EMBEDDINGS_MAX_INPUT_CHARS= # null = auto per model (2048 for Bedrock Cohere v3), 0 = off +# MEMORY_EMBEDDINGS_MAX_INPUT_CHARS= # null = auto per model (2048 for Bedrock Cohere v3), 0 = off +# MEMORY_RETAIN_CHUNK_SIZE=12288 # Chars per extraction chunk (< 64000) +# MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE=100 # Per-scope observation cap (-1 = unlimited, 0 = no new observations) + + +# ============================================================================= +# Cerebral Mental Models (Briefing Refresh) +# ============================================================================= +# Mental-model refresh synthesizes briefings after consolidation. +# Falls back to prefrontal.recall.model → cerebral.retain.model → default_model. +# Only used when prefrontal.reflects > 0. +# MEMORY_MENTAL_MODELS_MODEL= # Briefing refresh model +# MEMORY_MENTAL_MODELS_MAX_TOKENS=2048 # Briefing length +# MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS=0 # Minimum seconds between refreshes + +# ============================================================================= +# Prefrontal (Semantic Recall into Agent Context) +# ============================================================================= +# Reads Cerebral before each agent call (briefings + recalled facts) and gives +# RLM agents a recall_memory tool. Active per signature when memory is enabled +# and Cerebral is available. Read-only: never writes to Cerebral. +# Every recall is logged (INFO summary) and stored in the episodic `recalls` table. +# +# Both Prefrontal and briefing refreshes run at Hindsight's LOW budget with a +# doubled global cap (2 × reflects), so each gets exactly ``reflects`` iterations. +# A raw HINDSIGHT_API_REFLECT_MAX_ITERATIONS env value is used as-is (not doubled). +# MEMORY_PREFRONTAL_REFLECTS=3 # Reflect iterations; 0 = raw facts, no LLM, no briefings + +# Recall settings (read-time context injection) +# MEMORY_RECALL_MODEL= # Reflect model; also briefings fallback unless cerebral.mental_models.model set +# MEMORY_RECALL_REACH=org # local | org | bank (bank crosses organisations) +# MEMORY_RECALL_MAX_TOKENS=8192 # Pre-call context budget +# MEMORY_RECALL_MAX_TOOL_TOKENS=2048 # Tool result budget +# MEMORY_RECALL_MAX_TOOL_CALLS=0 # Per agent call (0 = recall_memory tool off) + +# Reflect LLM timeout/retries default to codespy's LLM_TIMEOUT / LLM_RETRIES. +# Override via HINDSIGHT_API_REFLECT_LLM_TIMEOUT / HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES. + + # ============================================================================= # Output Settings # ============================================================================= # Output format: markdown or json (default: markdown) -# OUTPUT_FORMAT=markdown +# REVIEW_OUTPUT_FORMAT=markdown # Enable stdout output (default: true) -# OUTPUT_STDOUT=true +# REVIEW_OUTPUT_STDOUT=true -# Post review to Git platform (GitHub PR or GitLab MR) (default: false) -# OUTPUT_GIT=false +# Post review to Git platform (GitHub PR or GitLab MR) (default: true) +# REVIEW_OUTPUT_GIT=true # Cache directory for cloned repositories (default: ~/.cache/codespy) -# CACHE_DIR=/path/to/cache +# REVIEW_CACHE_DIR=/path/to/cache + # ============================================================================= # Per-Signature Configuration (override via env vars) # ============================================================================= # Use underscore (_) between signature name and setting -# Format: SIGNATURE_NAME_SETTING=value +# Format: REVIEW__=value # # Available signatures: # - CODE_REVIEW (bugs, security, removed defensive code, code smells) # - DOC (compares patches against documentation) # - SCOPE (code scope detection) # - SUPPLY_CHAIN (supply chain security analysis) -# - SUMMARY (PR summary generation) +# - SUMMARY (PR summary generation) +# - AUDIT (quality assessment after reviews) # # Available settings per signature: # - ENABLED (true/false) # - MAX_ITERS (integer) -# - MAX_LLM_CALLS (integer) - RLM fallback max calls (unset -> DEFAULT_MAX_LLM_CALLS) +# - MAX_LLM_CALLS (integer) - RLM fallback max calls # - MODEL (LiteLLM model string) -# - REASONING_EFFORT (minimal|low|medium|high) - Provider reasoning budget -# - TEMPERATURE (float) - Must be 1 while reasoning is enabled -# - MAX_TOKENS (integer) - Output token budget (unset -> DEFAULT_MAX_TOKENS) -# - MEMORY_ENABLED (true/false) - Hippocampus memory (unset -> MEMORY_DEFAULT_ENABLED) +# - REASONING_EFFORT (minimal|low|medium|high) +# - TEMPERATURE (float) +# - MAX_TOKENS (integer) # Examples: -# CODE_REVIEW_ENABLED=true -# CODE_REVIEW_MAX_ITERS=4 -# CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 -# CODE_REVIEW_REASONING_EFFORT=medium -# CODE_REVIEW_TEMPERATURE=1 -# CODE_REVIEW_MAX_TOKENS=32000 - -# CODE_REVIEW_MEMORY_ENABLED=true - -# SUPPLY_CHAIN_ENABLED=true -# When true: scans ALL artifacts (Dockerfiles, etc.) and manifests -# When false (default): only scans artifacts/manifests modified in the MR -# SUPPLY_CHAIN_SCAN_UNCHANGED=false -# SUPPLY_CHAIN_MEMORY_ENABLED=true - -# DOC_ENABLED=true -# DOC_MAX_ITERS=1 -# DOC_MAX_LLM_CALLS=2 -# DOC_MODEL=anthropic/claude-sonnet-4-5-20250929 -# DOC_MEMORY_ENABLED=true - -# SCOPE_ENABLED=true -# SCOPE_MAX_ITERS=4 -# SCOPE_REASONING_EFFORT=low -# SCOPE_MAX_TOKENS=32000 -# SCOPE_MEMORY_ENABLED=true - -# SUMMARY_ENABLED=true -# SUMMARY_MAX_ITERS=1 -# SUMMARY_MAX_LLM_CALLS=2 -# SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001 -# SUMMARY_MEMORY_ENABLED=true - +# REVIEW_CODE_REVIEW_ENABLED=true +# REVIEW_CODE_REVIEW_MAX_ITERS=4 +# REVIEW_CODE_REVIEW_MODEL=anthropic/claude_sonnet-4-5-20250929 +# REVIEW_CODE_REVIEW_REASONING_EFFORT=medium +# REVIEW_CODE_REVIEW_TEMPERATURE=1 +# REVIEW_CODE_REVIEW_MAX_TOKENS=32000 + +# REVIEW_SUPPLY_CHAIN_ENABLED=true +# REVIEW_SUPPLY_CHAIN_SCAN_UNCHANGED=false + +# REVIEW_DOC_ENABLED=true +# REVIEW_DOC_MAX_ITERS=2 +# REVIEW_DOC_MAX_LLM_CALLS=4 +# REVIEW_DOC_MODEL=bedrock/converse/global.anthropic.claude-opus-5-5 + +# REVIEW_SCOPE_SKIP_REFINEMENT_WHEN_CLEAN=true + +# REVIEW_SCOPE_ENABLED=true +# REVIEW_SCOPE_MAX_ITERS=4 +# REVIEW_SCOPE_REASONING_EFFORT=low +# REVIEW_SCOPE_MAX_TOKENS=32000 + +# REVIEW_SUMMARY_ENABLED=true +# REVIEW_SUMMARY_MAX_ITERS=1 +# REVIEW_SUMMARY_MAX_LLM_CALLS=2 +# REVIEW_SUMMARY_MODEL=bedrock/converse/nvidia.nemotron-super-3-120b + +# REVIEW_AUDIT_ENABLED=true +# REVIEW_AUDIT_MAX_ITERS=1 diff --git a/.github/workflows/codespy-review.yml.example b/.github/workflows/codespy-review.yml.example index dcb70ac..bfca331 100644 --- a/.github/workflows/codespy-review.yml.example +++ b/.github/workflows/codespy-review.yml.example @@ -40,7 +40,7 @@ jobs: # 💰 Cheap tier — summarization model: 'anthropic/claude-opus-4-6' extraction-model: 'anthropic/claude-sonnet-4-5-20250929' - summarization-model: 'anthropic/claude-haiku-4-5-20251001' + summary-model: 'anthropic/claude-haiku-4-5-20251001' # Required: Provide your LLM API key (use GitHub Secrets!) anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -71,7 +71,7 @@ jobs: with: model: 'anthropic/claude-opus-4-6' extraction-model: 'anthropic/claude-sonnet-4-5-20250929' - summarization-model: 'anthropic/claude-haiku-4-5-20251001' + summary-model: 'anthropic/claude-haiku-4-5-20251001' anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} pr-url: ${{ github.event.issue.pull_request.html_url }} @@ -93,7 +93,7 @@ jobs: # with: # model: 'bedrock/us.anthropic.claude-opus-4-6-v1' # extraction-model: 'bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0' -# summarization-model: 'bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0' +# summary-model: 'bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0' # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # aws-region: 'us-east-1' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4abef75..b136da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,99 @@ ## [Unreleased] +## [2.0.0] - 2026-10-03 + +### Changed (Breaking) + +- **Configuration structure** — `Settings` is now `llm` / `github` / `gitlab` / `memory` / `review` with `extra="ignore"`. A 1.2.4 `codespy.yaml` is silently ignored for every moved key. Migration: + - Top-level `default_model`, `extraction_model`, `default_max_iters`, `default_max_llm_calls`, `default_reasoning_effort`, `default_temperature`, `default_max_tokens` → `llm.*`. `llm_retries`/`llm_timeout` → `llm.retries`/`llm.timeout`. `rlm_fallback` → `llm.rlm_fallback`. + - `signatures.` → `review.`. `min_confidence`, `output_format`, `output_stdout`, `output_git`, `cache_dir`, `excluded_directories` → `review.*`. + - `memory.default_enabled` → `memory.enabled`. `memory.{compact_trajectory,max_trajectory_tokens,max_question_tokens,distiller,cartographer}` → `memory.hippocampus.*`. `memory.max_context_memory_tokens`/`max_context_item_tokens` → `memory.hippocampus.max_hippocampus_tokens`/`max_hippocampus_item_tokens`. + - Env vars: `_*` → `REVIEW__*`. `MIN_CONFIDENCE`, `OUTPUT_FORMAT`, `OUTPUT_STDOUT`, `OUTPUT_GIT`, `CACHE_DIR`, `EXCLUDED_DIRECTORIES` → `REVIEW_*`. `MEMORY_DEFAULT_ENABLED` → `MEMORY_ENABLED`. `MEMORY_MAX_CONTEXT_MEMORY_TOKENS`/`MEMORY_MAX_CONTEXT_ITEM_TOKENS` → `MEMORY_MAX_HIPPOCAMPUS_TOKENS`/`MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS`. + - Unchanged: `DEFAULT_*`, `EXTRACTION_MODEL`, `LLM_RETRIES`, `LLM_TIMEOUT`, `RLM_FALLBACK_*`, `MEMORY_DISTILLER_*`, `MEMORY_CARTOGRAPHER_*`, `MEMORY_POSTGRES_*` (except `SCHEMA`), `MEMORY_PG0_*`, `MEMORY_BANK_ID`, `MEMORY_COMPACT_TRAJECTORY`, `MEMORY_MAX_TRAJECTORY_TOKENS`, `MEMORY_MAX_QUESTION_TOKENS`, and provider keys. +- **New default models and temperature** — out-of-the-box codespy now requires AWS Bedrock access to Opus 5.5 and both Nemotron models: + - `DEFAULT_MODEL`: `bedrock/converse/nvidia.nemotron-nano-3-30b` + - `EXTRACTION_MODEL`: `bedrock/converse/nvidia.nemotron-nano-3-30b` + - `REVIEW_SCOPE_MODEL`, `REVIEW_CODE_REVIEW_MODEL`, `REVIEW_DOC_MODEL`, `REVIEW_AUDIT_MODEL`: `bedrock/converse/global.anthropic.claude-opus-5-5` + - `REVIEW_SUPPLY_CHAIN_MODEL`, `REVIEW_SUMMARY_MODEL`: `bedrock/converse/nvidia.nemotron-super-3-120b` + - `MEMORY_*_MODEL` (distiller, cartographer, retain, consolidation, mental_models, recall): `bedrock/converse/nvidia.nemotron-super-3-120b` + - `MEMORY_EMBEDDINGS_MODEL`: `bedrock/cohere.embed-v4:0` + - `DEFAULT_TEMPERATURE`: `1.0` +- **Embeddings model v4** changes the vector dimension — requires a semantic DB reset (`DROP SCHEMA semantic CASCADE`) +- **Default `MEMORY_BANK_ID` changed from `codespy` to `codebase`** — set `MEMORY_BANK_ID=codespy` to keep using existing memory data +- **Memory disabled by default** — `memory.enabled` now defaults to `false`. Previously `review.summary.memory.enabled: true` in main `codespy.yaml` caused summary episodes to save even with global memory off. Now only the global switch gates memory, so nothing is saved by default. +- **Removed `memory.postgres.schema`** — episodic schema is now hard-coded to `episodic` (like `semantic`). The `MEMORY_POSTGRES_SCHEMA` env var is removed. +- **Action `model` input is now optional** (was `required: true`); when unset, codespy.yaml default applies. Action inputs no longer carry `default:` values; codespy.yaml applies instead. Setting a pinned unit to `null` (YAML `model: null` or env `VAR=null`) re-enables the fallback chain. Migration note: upgrade the action ref and `codespy-version` together. An older action with the `latest` image, or the reverse, silently drops review settings. +- **EVICT tombstones** — Hippocampus eviction writes `EVICT` mutation tombstones (`MutationType.EVICT`, separate from `DELETE`). `load_context` excludes both `DELETE` and `EVICT`. ROLLBACK NOTE: loaders older than this release only skip `DELETE` and fail to load a topic that has an `EVICT` row. Before rolling back, run `UPDATE observations SET op_type = 'DELETE' WHERE op_type = 'EVICT';` (both are content-NULL tombstones, so no data is lost). +- **Dependencies**: `hindsight-api-slim >=0.10.1` (new); `mcp` upgraded from `>=1.29,<2` to `>=2.2,<3` + +### Added + +- **Cerebral (Semantic Memory)**: New `codespy/agents/memory/cerebral/` package with mutation-based retain (ADD/`supersedes`/`RETRACTED`), domain-agnostic missions, tags/scopes (`project_scope:`, `repo:`, `org:`, `task:`), once-per-run consolidation via `SyncTaskBackend`, `max_observations_per_scope`, mental-model briefings in delta mode with `memory.cerebral.mental_models.*`, embeddings + `max_input_chars`, structured-output support, read API (`arecall`/`recall`, `areflect`/`reflect`, `aget_mental_models`, `aget_observation_history`), and fixed `semantic` schema. +- **Prefrontal (Semantic Recall)**: New `codespy/agents/memory/prefrontal/` package with run-level recall shared by summary, code_review, doc, supply_chain, and audit agents. Before each agent call it reads Cerebral and injects a read-only `prefrontal_memory` input (mental-model briefings, five question-shaped recall facets — context, seen before, decisions, recurring patterns, what changed — and facts from other repositories). Configured under `memory.prefrontal` (`MEMORY_PREFRONTAL_REFLECTS`, `MEMORY_RECALL_*`). The `recall_memory` tool is available to RLM agents (code_review, scope, supply_chain) but is off by default (`max_tool_calls: 0`). Prefrontal never writes to Cerebral and never feeds Hippocampus. +- **Review memories section**: The pre-call Prefrontal memory (run-level and scope-level) now appears in a collapsed `memories` section **before** the Summary. Run-level sections (Briefings, Around this work, Seen before, Decisions, Recurring patterns, What changed, Other repositories) render directly under `memories`. Scope recall, when present, is nested in its own `scope` block. All levels are indented with `
` so nesting is visible in GitHub/GitLab. This section is shown in both the GitHub/GitLab review body and the stdout/MCP markdown output. +- **New action inputs** (no defaults, values taken from codespy.yaml when unset): + - `scope-max-tokens`, `code-review-max-tokens`, `doc-max-tokens`, `supply-chain-max-tokens`, `summary-max-tokens`, `audit-max-tokens` + - `memory-distiller-max-tokens`, `memory-cartographer-max-tokens` + - `memory-compact-trajectory`, `memory-max-hippocampus-tokens`, `memory-max-hippocampus-item-tokens`, `memory-max-trajectory-tokens`, `memory-max-question-tokens` + - `memory-retain-model`, `memory-retain-chunk-size` + - `memory-consolidation-model`, `memory-consolidation-max-observations-per-scope` + - `memory-mental-models-model`, `memory-mental-models-max-tokens`, `memory-mental-models-min-refresh-seconds` + - `memory-recall-model`, `memory-recall-reach`, `memory-recall-max-tokens`, `memory-recall-max-tool-tokens`, `memory-recall-max-tool-calls` + - `memory-prefrontal-reflects`, `memory-embeddings-model`, `memory-embeddings-max-input-chars` + - `scope-skip-refinement-when-clean` + - `openai-api-base`, `azure-api-key`, `azure-api-base`, `azure-api-version` +- **AWS region forwarding**: AWS region is now forwarded independently of access key (was bundled with credentials) +- **Cost buckets**: `memory_recall`, `memory_recall_embeddings`, `memory_consolidation`, `memory_mental_models` +- **ReflectSummary monitoring**: `areflect` returns `(text, ReflectSummary)` with iterations, LLM calls, map calls, rewrite flag, tools used, usage, and empty flag. Stored in `RecallRecord.details["reflect"]` for monitoring. + +### Changed + +- **Review statistics matrix**: the GitHub/GitLab review Statistics table is now a severity × category matrix (Security, Bugs, Documentation, Smells + totals); Smells are now counted. +- **Review publish order**: Episode saves, retain, consolidation and mental-model refresh run after audit. For remote reviews, the GitHub/GitLab review is posted first with "Memory: pending", then edited with full costs after the memory phase completes. Local CLI and MCP reviews run the memory phase before returning/printing. +- **Cost breakdown split**: The per-signature cost table is split into a Review table and a Memory table (memory_* buckets), each with its own subtotal. +- **Cost table rows renamed** (1.2.4 rows `distiller`/`cartographer` → `memory_distiller`/`memory_cartographer`) +- **Cerebral tags**: Removed `episode:` and `run_id:` tags — they were not used for scoping or reach and bloated the tag set. `pull_request:` tags are also no longer added. Retained items now only carry `project_scope:`, `repo:`, `org:`, and `task:` tags. +- **Cerebral fallback scope**: Episodes without a `project_scope` topic now consolidate into `[org:, repo:, project_scope:]` (the repo-root project scope) instead of `[org:, repo:]`. This ensures every write scope matches the observation cap rule and creates/updates the repo-root briefing. +- **Cerebral retain framing**: Moved from `context` to `metadata`. The `context` field is no longer set on retain items; instead `metadata` carries `task`, `repo` (when known), `kind` (`"observation changes"` / `"artifacts"`), and `question`. This reduces consolidation input size. +- **Doc review CHANGELOG trim**: CHANGELOG files are now trimmed to preamble + 2 newest sections (keeps `[Unreleased]` + latest release). Saves ~25KB (21%) of the ~118KB doc payload in this repo. +- **load_context semantics**: Now loads the latest version of every observation from ALL prior episodes (not just the latest episode), excluding tombstones (`DELETE`, `EVICT`) and any version with no content. Observations are returned oldest-first, so eviction drops the oldest facts first. Topic prefix matching now uses proper LIKE escaping so `owner/repo` no longer matches `owner/repo-other`. +- **Cerebral DELETE vs EVICT handling**: DELETE produces a `RETRACTED` line marking facts as wrong; EVICT of unchanged prior facts sends nothing; content new this run (ADD→EVICT or REPLACE→EVICT) is retained to avoid losing newly learned facts. + +### Removed + +- **Per-signature memory overrides** — `review..memory.enabled` (YAML), `REVIEW__MEMORY_ENABLED` (env), and action inputs `scope-memory-enabled`, `code-review-memory-enabled`, `doc-memory-enabled`, `supply-chain-memory-enabled`, `summary-memory-enabled`, `audit-memory-enabled` are removed. Memory is now controlled only by the global `memory.enabled` (`MEMORY_ENABLED`, `memory-enabled`). +- Action input `memory-prefrontal-model` (was deprecated, never wired). The real input is `memory-recall-model`. +- Action env vars: `SCOPE_MEMORY_ENABLED`, `CODE_REVIEW_MEMORY_ENABLED`, `DOC_MEMORY_ENABLED`, `SUPPLY_CHAIN_MEMORY_ENABLED`, `SUMMARY_MEMORY_ENABLED`, `AUDIT_MEMORY_ENABLED`, `OUTPUT_FORMAT` +- Config settings: `enable_prompt_caching`/`ENABLE_PROMPT_CACHING`/`enable-prompt-caching`, `compact_patches`/`COMPACT_PATCHES`/`compact-patches` +- Tree-sitter patch compaction (`tools/git/patch_utils.py` deleted) +- Dead code: `Settings.sync_llm_settings()`, `LLMConfig.sync_from_flat()`, `Settings.get_memory_enabled()`, `get_memory_budget()` no longer takes an argument, `MemorySignatureConfig`, `SIGNATURE_PREFIXES`, `SIGNATURE_SETTINGS`, `MEMORY_SIGNATURE_SETTINGS`, `RLM_FALLBACK_ENV_SETTINGS` +- Agents no longer receive memory input: `inject_context_memory` and the `context_memory=` argument have been removed from all six review agents (summary, scope, code_review, doc, supply_chain, audit). Memory is learned but not used directly; prior knowledge now reaches agents only through Prefrontal. + +### Fixed + +- `recall_memory` with `reach=local` returned nothing for code_review and supply_chain; the tool now searches the reviewed project scope +- "too many values to unpack" error in review statistics matrix (line 345 was iterating `severities` as tuples instead of enums) +- `GITLAB_URL` from the environment was ignored — now properly takes precedence over YAML `gitlab.url` +- Token precedence now matches documentation: env/`.env` beats YAML (`github.token` / `gitlab.token`) +- Removed action `llm-timeout` default (was `'120'`, now uses codespy.yaml's `240`) +- Doc reviewer defaults now match codespy.yaml (`max_iters: 2`, `max_llm_calls: 4`) +- Fixed workflow example input: `summarization-model` → `summary-model` +- Prefrontal reads on a bank with nothing retained yet (a new `MEMORY_BANK_ID`) now return empty results instead of failing with `Bank '' not found` +- Retain no longer fails with "all N facts returned by the LLM were unusable" on Nemotron 3 Super (Bedrock) — Cerebral now registers the structured-output capability and strips JSON Schema keywords Bedrock's native structured output rejects +- Briefing (mental model) delta refreshes no longer fail with "operations must be a list, got " — Cerebral now decodes string-encoded operations before Hindsight validates them +- `Missing required inputs: ['prefrontal_memory']` regression: Fixed a bug where agents failed when the Prefrontal instance existed but the shared run-level `prefrontal_memory` text was empty (fresh DB). The signature is now chosen based on whether there is text to pass +- Cerebral consolidation and retain LLM calls now use `llm.timeout` / `llm.retries`. Previously only reflect did, so consolidation timed out at Hindsight's 120 s default. +- Cerebral consolidation no longer logs `relation "webhooks" does not exist` — inline (`SyncTaskBackend`) tasks now carry the `semantic` schema +- Cerebral consolidation no longer fails at exit with `cannot schedule new futures after shutdown` — background episode saves now finish before Python shuts down its thread pools +- Briefings no longer stay stuck on "Generating content..." after a failed refresh +- Embedding inputs are now capped for Bedrock Cohere v3 (2048 characters) to prevent `maxLength` errors +- Cost calculation: `TwoStepAdapter` extraction calls are now included in signature cost tracking +- Cost billing: DSPy cache hits (`response.cache_hit`) are now skipped in cost accounting +- Cost split calculation: When `entry["cost"]` is available (billed cost), input cost is now calculated as `billed_cost - output_cost` +- `DOC_MAX_ITERS`/`DOC_MAX_LLM_CALLS` env passthrough now works +- `memory-enabled`→`MEMORY_ENABLED` mapping in action + ## [1.2.4] - 2026-09-18 ### Changed diff --git a/README.md b/README.md index 31280ac..0bbe596 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ codespy auto-discovers credentials from standard locations (`~/.aws/credentials` | **[Usage](docs/usage.md)** | CLI commands, Docker, GitHub Action, MCP server, output formats | | **[Configuration](docs/configuration.md)** | Environment variables, YAML config, model strategy, per-signature settings | | **[Architecture](docs/architecture.md)** | Pipeline design, DSPy signatures, supported languages | -| **[Memory System](docs/memory.md)** | Hippocampus episodic memory for cross-review knowledge | +| **[Memory System](docs/memory.md)** | Hippocampus (episodic), Cerebral (semantic store), Prefrontal (recall) | | **[Development](docs/development.md)** | Setup, build, test, lint | --- diff --git a/action.yml b/action.yml index 4796d8d..b43c252 100644 --- a/action.yml +++ b/action.yml @@ -12,11 +12,11 @@ inputs: default: ${{ github.token }} model: - description: 'Default LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5, bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0)' - required: true + description: 'Default LLM model to use (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-5, bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0). Affects units without a model pin. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-nano-3-30b' + required: false extraction-model: - description: 'Model for field extraction (smaller model recommended)' + description: 'Model for field extraction (smaller model recommended). Default from codespy.yaml: bedrock/converse/nvidia.nemotron-nano-3-30b' required: false pr-url: @@ -30,9 +30,8 @@ inputs: default: 'true' output-format: - description: 'Output format: markdown or json' + description: 'Output format: markdown or json. Note: issue count parsing only understands markdown. Default from codespy.yaml: markdown' required: false - default: 'markdown' # LLM Provider API Keys anthropic-api-key: @@ -51,360 +50,423 @@ inputs: aws-access-key-id: description: 'AWS Access Key ID (for Bedrock models)' required: false - + aws-secret-access-key: description: 'AWS Secret Access Key (for Bedrock models)' required: false - + aws-region: - description: 'AWS Region (for Bedrock models)' + description: 'AWS Region (for Bedrock models). Default from codespy.yaml: us-east-1' required: false - default: 'us-east-1' - + # Global defaults default-max-iters: - description: 'Default maximum iterations for all signatures' + description: 'Default maximum iterations for all signatures. Default from codespy.yaml: 5' required: false - default: '5' - + default-reasoning-effort: - description: 'Default reasoning effort for all signatures (minimal|low|medium|high)' + description: 'Default reasoning effort for all signatures (minimal|low|medium|high). Default from codespy.yaml: medium' required: false - default: 'medium' - + default-temperature: - description: 'Default temperature for LLM calls' + description: 'Default temperature for LLM calls. Default from codespy.yaml: 1.0' required: false - default: '0.2' default-max-tokens: - description: 'Output token budget per completion (reasoning tokens included). Clamped to model ceiling.' + description: 'Output token budget per completion (reasoning tokens included). Clamped to model ceiling. Default from codespy.yaml: 32000' required: false - default: '32000' llm-retries: - description: 'Number of retries for LLM API calls' + description: 'Number of retries for LLM API calls. Default from codespy.yaml: 2' required: false - default: '2' - + llm-timeout: - description: 'Timeout in seconds for LLM calls' + description: 'Timeout in seconds for LLM calls. Default from codespy.yaml: 240' required: false - default: '120' - - enable-prompt-caching: - description: 'Enable provider-side prompt caching (reduces latency and costs)' - required: false - default: 'true' default-max-llm-calls: - description: 'Maximum LLM calls for RLM fallback (context overflow recovery)' + description: 'Maximum LLM calls for RLM fallback (context overflow recovery). Default from codespy.yaml: 8' required: false - default: '8' # RLM fallback (context rot prevention) rlm-fallback-enabled: - description: 'Enable proactive RLM fallback before context window overflow' + description: 'Enable proactive RLM fallback before context window overflow. Default from codespy.yaml: true' required: false - default: 'true' rlm-fallback-react-threshold: - description: 'Context ratio for ReAct modules (0.0-1.0). RLM used when input exceeds this ratio of max_input_tokens.' + description: 'Context ratio for ReAct modules (0.0-1.0). RLM used when input exceeds this ratio of max_input_tokens. Default from codespy.yaml: 0.30' required: false - default: '0.30' rlm-fallback-chain-of-thought-threshold: - description: 'Context ratio for ChainOfThought modules (0.0-1.0). RLM used when input exceeds this ratio.' + description: 'Context ratio for ChainOfThought modules (0.0-1.0). RLM used when input exceeds this ratio. Default from codespy.yaml: 0.40' required: false - default: '0.40' rlm-fallback-predict-threshold: - description: 'Context ratio for Predict modules (0.0-1.0). RLM used when input exceeds this ratio.' + description: 'Context ratio for Predict modules (0.0-1.0). RLM used when input exceeds this ratio. Default from codespy.yaml: 0.50' required: false - default: '0.50' min-confidence: - description: 'Minimum confidence threshold (0.0-1.0) for reported issues' + description: 'Minimum confidence threshold (0.0-1.0) for reported issues. Default from codespy.yaml: 0.81' 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 # ========================================== scope-enabled: - description: 'Enable scope identification signature' + description: 'Enable scope identification signature. Default from codespy.yaml: true' required: false - default: 'true' - + scope-model: - description: 'Model for scope identification (empty = use default)' + description: 'Model for scope identification (empty = use default). Default from codespy.yaml: bedrock/converse/global.anthropic.claude-opus-5-5' required: false - + scope-max-iters: - description: 'Max iterations for scope identification' + description: 'Max iterations for scope identification. Default from codespy.yaml: 3' required: false scope-max-llm-calls: - description: 'Max LLM calls for scope RLM fallback' + description: 'Max LLM calls for scope RLM fallback. Default from codespy.yaml: 5' + required: false + + scope-max-tokens: + description: 'Max tokens for scope identification. Default from codespy.yaml: default-max-tokens' required: false scope-reasoning-effort: - description: 'Reasoning effort for scope (minimal|low|medium|high)' + description: 'Reasoning effort for scope (minimal|low|medium|high). Default from codespy.yaml: medium' required: false - + + scope-skip-refinement-when-clean: + description: 'Skip LLM refinement when deterministic resolution is clean. Default from codespy.yaml: true' + required: false + scope-temperature: - description: 'Temperature for scope identification' + description: 'Temperature for scope identification. Default from codespy.yaml: 1.0' required: false - + # ========================================== # SIGNATURE: code_review # ========================================== code-review-enabled: - description: 'Enable code review signature (bugs, security vulnerabilities, code smells)' + description: 'Enable code review signature (bugs, security vulnerabilities, code smells). Default from codespy.yaml: true' required: false - default: 'true' - + code-review-model: - description: 'Model for code review (empty = use default)' + description: 'Model for code review (empty = use default). Default from codespy.yaml: bedrock/converse/global.anthropic.claude-opus-5-5' required: false - + code-review-max-iters: - description: 'Max iterations for code review' + description: 'Max iterations for code review. Default from codespy.yaml: 5' required: false code-review-max-llm-calls: - description: 'Max LLM calls for code review RLM fallback' + description: 'Max LLM calls for code review RLM fallback. Default from codespy.yaml: 8' + required: false + + code-review-max-tokens: + description: 'Max tokens for code review. Default from codespy.yaml: default-max-tokens' required: false code-review-reasoning-effort: - description: 'Reasoning effort for code review (minimal|low|medium|high)' + description: 'Reasoning effort for code review (minimal|low|medium|high). Default from codespy.yaml: medium' required: false - + code-review-temperature: - description: 'Temperature for code review' + description: 'Temperature for code review. Default from codespy.yaml: 1.0' required: false - + # ========================================== # SIGNATURE: doc # ========================================== doc-enabled: - description: 'Enable documentation review signature (stale/wrong documentation)' + description: 'Enable documentation review signature (stale/wrong documentation). Default from codespy.yaml: true' required: false - default: 'true' - + doc-model: - description: 'Model for doc review (empty = use default)' + description: 'Model for doc review (empty = use default). Default from codespy.yaml: bedrock/converse/global.anthropic.claude-opus-5-5' required: false doc-max-iters: - description: 'Max iterations for doc review' + description: 'Max iterations for doc review. Default from codespy.yaml: 2' required: false doc-max-llm-calls: - description: 'Max LLM calls for doc review RLM fallback' + description: 'Max LLM calls for doc review RLM fallback. Default from codespy.yaml: 4' + required: false + + doc-max-tokens: + description: 'Max tokens for doc review. Default from codespy.yaml: default-max-tokens' required: false doc-reasoning-effort: - description: 'Reasoning effort for doc (minimal|low|medium|high)' + description: 'Reasoning effort for doc (minimal|low|medium|high). Default from codespy.yaml: medium' required: false - + doc-temperature: - description: 'Temperature for doc review' + description: 'Temperature for doc review. Default from codespy.yaml: 1.0' required: false - + # ========================================== # SIGNATURE: supply_chain # ========================================== supply-chain-enabled: - description: 'Enable supply chain security signature' + description: 'Enable supply chain security signature. Default from codespy.yaml: true' required: false - default: 'true' - + supply-chain-model: - description: 'Model for supply chain security (empty = use default)' + description: 'Model for supply chain security (empty = use default). Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false - + supply-chain-max-iters: - description: 'Max iterations for supply chain security' + description: 'Max iterations for supply chain security. Default from codespy.yaml: null (use default-max-iters)' required: false supply-chain-max-llm-calls: - description: 'Max LLM calls for supply chain RLM fallback' + description: 'Max LLM calls for supply chain RLM fallback. Default from codespy.yaml: null (use default-max-llm-calls)' + required: false + + supply-chain-max-tokens: + description: 'Max tokens for supply chain. Default from codespy.yaml: default-max-tokens' required: false supply-chain-reasoning-effort: - description: 'Reasoning effort for supply chain (minimal|low|medium|high)' + description: 'Reasoning effort for supply chain (minimal|low|medium|high). Default from codespy.yaml: medium' required: false - + supply-chain-temperature: - description: 'Temperature for supply chain security' + description: 'Temperature for supply chain security. Default from codespy.yaml: 1.0' required: false - + supply-chain-scan-unchanged: - description: 'Scan all artifacts/manifests even if not modified in the PR (default: only scan changed)' + description: 'Scan all artifacts/manifests even if not modified in the PR (default: only scan changed). Default from codespy.yaml: false' required: false - default: 'false' - + # ========================================== # SIGNATURE: summary # ========================================== summary-enabled: - description: 'Enable PR summary signature' + description: 'Enable PR summary signature. Default from codespy.yaml: true' required: false - default: 'true' summary-max-iters: - description: 'Max iterations for summary' + description: 'Max iterations for summary. Default from codespy.yaml: 1' required: false summary-model: - description: 'Model for summary (empty = use default)' + description: 'Model for summary (empty = use default). Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false summary-max-llm-calls: - description: 'Max LLM calls for summary RLM fallback' + description: 'Max LLM calls for summary RLM fallback. Default from codespy.yaml: 2' + required: false + + summary-max-tokens: + description: 'Max tokens for summary. Default from codespy.yaml: default-max-tokens' required: false summary-reasoning-effort: - description: 'Reasoning effort for summary (minimal|low|medium|high)' + description: 'Reasoning effort for summary (minimal|low|medium|high). Default from codespy.yaml: medium' required: false summary-temperature: - description: 'Temperature for summary' + description: 'Temperature for summary. Default from codespy.yaml: 1.0' required: false # ========================================== # SIGNATURE: audit # ========================================== audit-enabled: - description: 'Enable audit signature (quality assessment + recommendation)' + description: 'Enable audit signature (quality assessment + recommendation). Default from codespy.yaml: true' required: false - default: 'true' audit-model: - description: 'Model for audit (empty = use default)' + description: 'Model for audit (empty = use default). Default from codespy.yaml: bedrock/converse/global.anthropic.claude-opus-5-5' required: false audit-max-iters: - description: 'Max iterations for audit' + description: 'Max iterations for audit. Default from codespy.yaml: 1' required: false audit-max-llm-calls: - description: 'Max LLM calls for audit RLM fallback' + description: 'Max LLM calls for audit RLM fallback. Default from codespy.yaml: 2' + required: false + + audit-max-tokens: + description: 'Max tokens for audit. Default from codespy.yaml: default-max-tokens' required: false audit-reasoning-effort: - description: 'Reasoning effort for audit (minimal|low|medium|high)' + description: 'Reasoning effort for audit (minimal|low|medium|high). Default from codespy.yaml: medium' required: false audit-temperature: - description: 'Temperature for audit' + description: 'Temperature for audit. Default from codespy.yaml: 1.0' required: false # ========================================== # MEMORY (Hippocampus) # ========================================== memory-enabled: - description: 'Enable memory (Hippocampus) globally for cross-run learning (default: false)' + description: 'Enable memory (Hippocampus) globally for cross-run learning. Default from codespy.yaml: false' required: false - default: 'false' 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)' + description: 'PostgreSQL port. Default from codespy.yaml: 5432' required: false - default: '5432' + memory-postgres-user: - description: 'PostgreSQL user (default: postgres)' + description: 'PostgreSQL user (falls back to postgres if unset)' required: false + memory-postgres-password: description: 'PostgreSQL password' required: false + memory-postgres-database: - description: 'PostgreSQL database name (default: codespy)' + description: 'PostgreSQL database name. Default from codespy.yaml: codespy' required: false - default: 'codespy' - memory-postgres-schema: - 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".' + description: 'Bank ID scoping all memory data (e.g., project name or agent name). Default from codespy.yaml: codebase' + required: false + + memory-compact-trajectory: + description: 'Apply head+tail trajectory bounding before distillation. Default from codespy.yaml: true' + required: false + + memory-max-hippocampus-tokens: + description: 'Ceiling on persisted context memory. Default from codespy.yaml: 16384' + required: false + + memory-max-hippocampus-item-tokens: + description: 'Soft per-observation token limit. Default from codespy.yaml: 512' + required: false + + memory-max-trajectory-tokens: + description: 'Cap on trajectory fed to Distiller. Default from codespy.yaml: 16384' + required: false + + memory-max-question-tokens: + description: 'Cap on serialized inputs as reflection question. Default from codespy.yaml: 8192' required: false memory-distiller-model: - description: 'Model for the Distiller reflection module (smaller model recommended)' + description: 'Model for the Distiller reflection module. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false memory-distiller-reasoning-effort: - description: 'Reasoning effort for Distiller (minimal|low|medium|high)' + description: 'Reasoning effort for Distiller (minimal|low|medium|high). Default from codespy.yaml: medium' required: false memory-distiller-temperature: - description: 'Temperature for Distiller' + description: 'Temperature for Distiller. Default from codespy.yaml: 1.0' + required: false + + memory-distiller-max-tokens: + description: 'Max tokens for Distiller. Default from codespy.yaml: default-max-tokens' required: false memory-distiller-max-iters: - description: 'Max RLM iterations for Distiller (default: 1)' + description: 'Max RLM iterations for Distiller. Default from codespy.yaml: 1' required: false memory-distiller-max-llm-calls: - description: 'Max RLM LLM calls for Distiller (default: 2)' + description: 'Max RLM LLM calls for Distiller. Default from codespy.yaml: 2' required: false memory-cartographer-model: - description: 'Model for the Cartographer reflection module (smaller model recommended)' + description: 'Model for the Cartographer reflection module. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false memory-cartographer-reasoning-effort: - description: 'Reasoning effort for Cartographer (minimal|low|medium|high)' + description: 'Reasoning effort for Cartographer (minimal|low|medium|high). Default from codespy.yaml: medium' required: false memory-cartographer-temperature: - description: 'Temperature for Cartographer' + description: 'Temperature for Cartographer. Default from codespy.yaml: 1.0' + required: false + + memory-cartographer-max-tokens: + description: 'Max tokens for Cartographer. Default from codespy.yaml: default-max-tokens' required: false memory-cartographer-max-iters: - description: 'Max RLM iterations for Cartographer (default: 1)' + description: 'Max RLM iterations for Cartographer. Default from codespy.yaml: 1' required: false memory-cartographer-max-llm-calls: - description: 'Max RLM LLM calls for Cartographer (default: 2)' + description: 'Max RLM LLM calls for Cartographer. Default from codespy.yaml: 2' + required: false + + # ========================================== + # MEMORY (Cerebral — semantic memory) + # ========================================== + memory-retain-model: + description: 'Model for Cerebral fact extraction. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false - # Per-signature memory overrides - scope-memory-enabled: - description: 'Enable memory for scope identification (overrides global memory-enabled)' + memory-retain-chunk-size: + description: 'Chars per Cerebral fact-extraction chunk (must be < 64000). Default from codespy.yaml: 12288' required: false - code-review-memory-enabled: - description: 'Enable memory for code review (overrides global memory-enabled)' + memory-consolidation-model: + description: 'Model for Cerebral consolidation. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false - doc-memory-enabled: - description: 'Enable memory for doc review (overrides global memory-enabled)' + memory-consolidation-max-observations-per-scope: + description: 'Per-scope observation cap (-1 = unlimited, 0 = no new observations). Default from codespy.yaml: 100' required: false - supply-chain-memory-enabled: - description: 'Enable memory for supply chain (overrides global memory-enabled)' + memory-mental-models-model: + description: 'Model for mental-model briefing refresh. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' required: false - summary-memory-enabled: - description: 'Enable memory for summary (overrides global memory-enabled)' + memory-mental-models-max-tokens: + description: 'Briefing size. Default from codespy.yaml: 2048' required: false - audit-memory-enabled: - description: 'Enable memory for audit (overrides global memory-enabled)' + memory-mental-models-min-refresh-seconds: + description: 'Minimum seconds between automatic briefing refreshes. Default from codespy.yaml: 0' required: false + memory-recall-model: + description: 'Model for Prefrontal recall and briefing fallback. Default from codespy.yaml: bedrock/converse/nvidia.nemotron-super-3-120b' + required: false + + memory-recall-reach: + description: 'Recall reach: local (this repo only), org (same owner), or bank (all repos). Default from codespy.yaml: org' + required: false + + memory-recall-max-tokens: + description: 'Pre-call context token budget. Default from codespy.yaml: 8192' + required: false + + memory-recall-max-tool-tokens: + description: 'Recall memory tool result token budget. Default from codespy.yaml: 2048' + required: false + + memory-recall-max-tool-calls: + description: 'Recall memory tool calls per agent (0 = tool off). Default from codespy.yaml: 0' + required: false + + memory-prefrontal-reflects: + description: 'Reflect iterations for recall and briefings (0 = raw-fact recall, no LLM, no briefings). Default from codespy.yaml: 3' + required: false + + memory-embeddings-model: + description: 'litellm embedding model for Cerebral. Default from codespy.yaml: bedrock/cohere.embed-v4:0' + required: false + + memory-embeddings-max-input-chars: + description: 'Maximum characters per embedding input (null=auto per model, 0=off). Default from codespy.yaml: null (auto)' + required: false + + # Per-signature memory overrides (already defined above) + excluded-directories: description: 'JSON array of directories to exclude from review (e.g., ["vendor", "dist"])' required: false @@ -476,10 +538,9 @@ runs: DEFAULT_MAX_TOKENS: ${{ inputs.default-max-tokens }} LLM_RETRIES: ${{ inputs.llm-retries }} LLM_TIMEOUT: ${{ inputs.llm-timeout }} - 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 }} + REVIEW_MIN_CONFIDENCE: ${{ inputs.min-confidence }} + REVIEW_EXCLUDED_DIRECTORIES: ${{ inputs.excluded-directories }} # RLM fallback RLM_FALLBACK_ENABLED: ${{ inputs.rlm-fallback-enabled }} @@ -488,65 +549,65 @@ runs: RLM_FALLBACK_PREDICT_THRESHOLD: ${{ inputs.rlm-fallback-predict-threshold }} # Scope identification signature - SCOPE_ENABLED: ${{ inputs.scope-enabled }} - SCOPE_MODEL: ${{ inputs.scope-model }} - SCOPE_MAX_ITERS: ${{ inputs.scope-max-iters }} - SCOPE_MAX_LLM_CALLS: ${{ inputs.scope-max-llm-calls }} - SCOPE_REASONING_EFFORT: ${{ inputs.scope-reasoning-effort }} - SCOPE_TEMPERATURE: ${{ inputs.scope-temperature }} - + REVIEW_SCOPE_ENABLED: ${{ inputs.scope-enabled }} + REVIEW_SCOPE_MODEL: ${{ inputs.scope-model }} + REVIEW_SCOPE_MAX_ITERS: ${{ inputs.scope-max-iters }} + REVIEW_SCOPE_MAX_LLM_CALLS: ${{ inputs.scope-max-llm-calls }} + REVIEW_SCOPE_MAX_TOKENS: ${{ inputs.scope-max-tokens }} + REVIEW_SCOPE_REASONING_EFFORT: ${{ inputs.scope-reasoning-effort }} + REVIEW_SCOPE_TEMPERATURE: ${{ inputs.scope-temperature }} + REVIEW_SCOPE_SKIP_REFINEMENT_WHEN_CLEAN: ${{ inputs.scope-skip-refinement-when-clean }} + # Code review signature - CODE_REVIEW_ENABLED: ${{ inputs.code-review-enabled }} - CODE_REVIEW_MODEL: ${{ inputs.code-review-model }} - CODE_REVIEW_MAX_ITERS: ${{ inputs.code-review-max-iters }} - CODE_REVIEW_MAX_LLM_CALLS: ${{ inputs.code-review-max-llm-calls }} - CODE_REVIEW_REASONING_EFFORT: ${{ inputs.code-review-reasoning-effort }} - CODE_REVIEW_TEMPERATURE: ${{ inputs.code-review-temperature }} - + REVIEW_CODE_REVIEW_ENABLED: ${{ inputs.code-review-enabled }} + REVIEW_CODE_REVIEW_MODEL: ${{ inputs.code-review-model }} + REVIEW_CODE_REVIEW_MAX_ITERS: ${{ inputs.code-review-max-iters }} + REVIEW_CODE_REVIEW_MAX_LLM_CALLS: ${{ inputs.code-review-max-llm-calls }} + REVIEW_CODE_REVIEW_REASONING_EFFORT: ${{ inputs.code-review-reasoning-effort }} + REVIEW_CODE_REVIEW_TEMPERATURE: ${{ inputs.code-review-temperature }} + # Doc review signature - DOC_ENABLED: ${{ inputs.doc-enabled }} - DOC_MODEL: ${{ inputs.doc-model }} - DOC_MAX_ITERS: ${{ inputs.doc-max-iters }} - DOC_MAX_LLM_CALLS: ${{ inputs.doc-max-llm-calls }} - DOC_REASONING_EFFORT: ${{ inputs.doc-reasoning-effort }} - DOC_TEMPERATURE: ${{ inputs.doc-temperature }} - + REVIEW_DOC_ENABLED: ${{ inputs.doc-enabled }} + REVIEW_DOC_MODEL: ${{ inputs.doc-model }} + REVIEW_DOC_MAX_ITERS: ${{ inputs.doc-max-iters }} + REVIEW_DOC_MAX_LLM_CALLS: ${{ inputs.doc-max-llm-calls }} + REVIEW_DOC_REASONING_EFFORT: ${{ inputs.doc-reasoning-effort }} + REVIEW_DOC_TEMPERATURE: ${{ inputs.doc-temperature }} + # Supply chain signature - SUPPLY_CHAIN_ENABLED: ${{ inputs.supply-chain-enabled }} - SUPPLY_CHAIN_MODEL: ${{ inputs.supply-chain-model }} - SUPPLY_CHAIN_MAX_ITERS: ${{ inputs.supply-chain-max-iters }} - SUPPLY_CHAIN_MAX_LLM_CALLS: ${{ inputs.supply-chain-max-llm-calls }} - SUPPLY_CHAIN_REASONING_EFFORT: ${{ inputs.supply-chain-reasoning-effort }} - SUPPLY_CHAIN_TEMPERATURE: ${{ inputs.supply-chain-temperature }} - SUPPLY_CHAIN_SCAN_UNCHANGED: ${{ inputs.supply-chain-scan-unchanged }} - + REVIEW_SUPPLY_CHAIN_ENABLED: ${{ inputs.supply-chain-enabled }} + REVIEW_SUPPLY_CHAIN_MODEL: ${{ inputs.supply-chain-model }} + REVIEW_SUPPLY_CHAIN_MAX_ITERS: ${{ inputs.supply-chain-max-iters }} + REVIEW_SUPPLY_CHAIN_MAX_LLM_CALLS: ${{ inputs.supply-chain-max-llm-calls }} + REVIEW_SUPPLY_CHAIN_REASONING_EFFORT: ${{ inputs.supply-chain-reasoning-effort }} + REVIEW_SUPPLY_CHAIN_TEMPERATURE: ${{ inputs.supply-chain-temperature }} + REVIEW_SUPPLY_CHAIN_SCAN_UNCHANGED: ${{ inputs.supply-chain-scan-unchanged }} + # Summary signature - SUMMARY_ENABLED: ${{ inputs.summary-enabled }} - SUMMARY_MAX_ITERS: ${{ inputs.summary-max-iters }} - SUMMARY_MODEL: ${{ inputs.summary-model }} - SUMMARY_MAX_LLM_CALLS: ${{ inputs.summary-max-llm-calls }} - SUMMARY_REASONING_EFFORT: ${{ inputs.summary-reasoning-effort }} - SUMMARY_TEMPERATURE: ${{ inputs.summary-temperature }} + REVIEW_SUMMARY_ENABLED: ${{ inputs.summary-enabled }} + REVIEW_SUMMARY_MAX_ITERS: ${{ inputs.summary-max-iters }} + REVIEW_SUMMARY_MODEL: ${{ inputs.summary-model }} + REVIEW_SUMMARY_MAX_LLM_CALLS: ${{ inputs.summary-max-llm-calls }} + REVIEW_SUMMARY_REASONING_EFFORT: ${{ inputs.summary-reasoning-effort }} + REVIEW_SUMMARY_TEMPERATURE: ${{ inputs.summary-temperature }} # Audit signature - AUDIT_ENABLED: ${{ inputs.audit-enabled }} - AUDIT_MODEL: ${{ inputs.audit-model }} - AUDIT_MAX_ITERS: ${{ inputs.audit-max-iters }} - AUDIT_MAX_LLM_CALLS: ${{ inputs.audit-max-llm-calls }} - AUDIT_REASONING_EFFORT: ${{ inputs.audit-reasoning-effort }} - AUDIT_TEMPERATURE: ${{ inputs.audit-temperature }} + REVIEW_AUDIT_ENABLED: ${{ inputs.audit-enabled }} + REVIEW_AUDIT_MODEL: ${{ inputs.audit-model }} + REVIEW_AUDIT_MAX_ITERS: ${{ inputs.audit-max-iters }} + REVIEW_AUDIT_MAX_LLM_CALLS: ${{ inputs.audit-max-llm-calls }} + REVIEW_AUDIT_REASONING_EFFORT: ${{ inputs.audit-reasoning-effort }} + REVIEW_AUDIT_TEMPERATURE: ${{ inputs.audit-temperature }} - # Other settings - EXCLUDED_DIRECTORIES: ${{ inputs.excluded-directories }} + # Output settings (REVIEW_OUTPUT_FORMAT removed - only via CLI flag) # Memory (Hippocampus) - MEMORY_DEFAULT_ENABLED: ${{ inputs.memory-enabled }} + MEMORY_ENABLED: ${{ inputs.memory-enabled }} 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_DISTILLER_MODEL: ${{ inputs.memory-distiller-model }} MEMORY_DISTILLER_REASONING_EFFORT: ${{ inputs.memory-distiller-reasoning-effort }} @@ -558,12 +619,28 @@ runs: MEMORY_CARTOGRAPHER_TEMPERATURE: ${{ inputs.memory-cartographer-temperature }} MEMORY_CARTOGRAPHER_MAX_ITERS: ${{ inputs.memory-cartographer-max-iters }} MEMORY_CARTOGRAPHER_MAX_LLM_CALLS: ${{ inputs.memory-cartographer-max-llm-calls }} - SCOPE_MEMORY_ENABLED: ${{ inputs.scope-memory-enabled }} - CODE_REVIEW_MEMORY_ENABLED: ${{ inputs.code-review-memory-enabled }} - DOC_MEMORY_ENABLED: ${{ inputs.doc-memory-enabled }} - SUPPLY_CHAIN_MEMORY_ENABLED: ${{ inputs.supply-chain-memory-enabled }} - SUMMARY_MEMORY_ENABLED: ${{ inputs.summary-memory-enabled }} - AUDIT_MEMORY_ENABLED: ${{ inputs.audit-memory-enabled }} + MEMORY_RETAIN_MODEL: ${{ inputs.memory-retain-model }} + MEMORY_CONSOLIDATION_MODEL: ${{ inputs.memory-consolidation-model }} + MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE: ${{ inputs.memory-consolidation-max-observations-per-scope }} + MEMORY_COMPACT_TRAJECTORY: ${{ inputs.memory-compact-trajectory }} + MEMORY_MAX_HIPPOCAMPUS_TOKENS: ${{ inputs.memory-max-hippocampus-tokens }} + MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS: ${{ inputs.memory-max-hippocampus-item-tokens }} + MEMORY_MAX_TRAJECTORY_TOKENS: ${{ inputs.memory-max-trajectory-tokens }} + MEMORY_MAX_QUESTION_TOKENS: ${{ inputs.memory-max-question-tokens }} + MEMORY_DISTILLER_MAX_TOKENS: ${{ inputs.memory-distiller-max-tokens }} + MEMORY_CARTOGRAPHER_MAX_TOKENS: ${{ inputs.memory-cartographer-max-tokens }} + MEMORY_MENTAL_MODELS_MODEL: ${{ inputs.memory-mental-models-model }} + MEMORY_MENTAL_MODELS_MAX_TOKENS: ${{ inputs.memory-mental-models-max-tokens }} + MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS: ${{ inputs.memory-mental-models-min-refresh-seconds }} + MEMORY_EMBEDDINGS_MAX_INPUT_CHARS: ${{ inputs.memory-embeddings-max-input-chars }} + MEMORY_PREFRONTAL_REFLECTS: ${{ inputs.memory-prefrontal-reflects }} + MEMORY_RECALL_MODEL: ${{ inputs.memory-recall-model }} + MEMORY_RECALL_REACH: ${{ inputs.memory-recall-reach }} + MEMORY_RECALL_MAX_TOKENS: ${{ inputs.memory-recall-max-tokens }} + MEMORY_RECALL_MAX_TOOL_TOKENS: ${{ inputs.memory-recall-max-tool-tokens }} + MEMORY_RECALL_MAX_TOOL_CALLS: ${{ inputs.memory-recall-max-tool-calls }} + MEMORY_EMBEDDINGS_MODEL: ${{ inputs.memory-embeddings-model }} + MEMORY_RETAIN_CHUNK_SIZE: ${{ inputs.memory-retain-chunk-size }} run: | set -eo pipefail @@ -571,14 +648,16 @@ runs: DOCKER_ARGS="--rm --network host" # Core settings - DOCKER_ARGS="$DOCKER_ARGS -e GITHUB_TOKEN -e DEFAULT_MODEL -e OUTPUT_FORMAT" + DOCKER_ARGS="$DOCKER_ARGS -e GITHUB_TOKEN" + [ -n "$DEFAULT_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MODEL" [ -n "$EXTRACTION_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e EXTRACTION_MODEL" # Add optional API keys if provided [ -n "$ANTHROPIC_API_KEY" ] && DOCKER_ARGS="$DOCKER_ARGS -e ANTHROPIC_API_KEY" [ -n "$OPENAI_API_KEY" ] && DOCKER_ARGS="$DOCKER_ARGS -e OPENAI_API_KEY" [ -n "$GEMINI_API_KEY" ] && DOCKER_ARGS="$DOCKER_ARGS -e GEMINI_API_KEY" - [ -n "$AWS_ACCESS_KEY_ID" ] && DOCKER_ARGS="$DOCKER_ARGS -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION" + [ -n "$AWS_ACCESS_KEY_ID" ] && DOCKER_ARGS="$DOCKER_ARGS -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY" + [ -n "$AWS_REGION" ] && DOCKER_ARGS="$DOCKER_ARGS -e AWS_REGION" # Global defaults [ -n "$DEFAULT_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_ITERS" @@ -587,10 +666,9 @@ runs: [ -n "$DEFAULT_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_TOKENS" [ -n "$LLM_RETRIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e LLM_RETRIES" [ -n "$LLM_TIMEOUT" ] && DOCKER_ARGS="$DOCKER_ARGS -e LLM_TIMEOUT" - [ -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" + [ -n "$REVIEW_MIN_CONFIDENCE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_MIN_CONFIDENCE" + [ -n "$REVIEW_EXCLUDED_DIRECTORIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_EXCLUDED_DIRECTORIES" # RLM fallback [ -n "$RLM_FALLBACK_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_ENABLED" @@ -599,63 +677,77 @@ runs: [ -n "$RLM_FALLBACK_PREDICT_THRESHOLD" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_PREDICT_THRESHOLD" # Scope identification - [ -n "$SCOPE_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_ENABLED" - [ -n "$SCOPE_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MODEL" - [ -n "$SCOPE_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MAX_ITERS" - [ -n "$SCOPE_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MAX_LLM_CALLS" - [ -n "$SCOPE_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_REASONING_EFFORT" - [ -n "$SCOPE_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_TEMPERATURE" - + [ -n "$REVIEW_SCOPE_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_ENABLED" + [ -n "$REVIEW_SCOPE_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_MODEL" + [ -n "$REVIEW_SCOPE_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_MAX_ITERS" + [ -n "$REVIEW_SCOPE_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_MAX_LLM_CALLS" + [ -n "$REVIEW_SCOPE_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_MAX_TOKENS" + [ -n "$REVIEW_SCOPE_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_REASONING_EFFORT" + [ -n "$REVIEW_SCOPE_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_TEMPERATURE" + [ -n "$REVIEW_SCOPE_SKIP_REFINEMENT_WHEN_CLEAN" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_SKIP_REFINEMENT_WHEN_CLEAN" + # Code review - [ -n "$CODE_REVIEW_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_ENABLED" - [ -n "$CODE_REVIEW_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MODEL" - [ -n "$CODE_REVIEW_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MAX_ITERS" - [ -n "$CODE_REVIEW_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MAX_LLM_CALLS" - [ -n "$CODE_REVIEW_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_REASONING_EFFORT" - [ -n "$CODE_REVIEW_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_TEMPERATURE" - + [ -n "$REVIEW_CODE_REVIEW_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_ENABLED" + [ -n "$REVIEW_CODE_REVIEW_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_MODEL" + [ -n "$REVIEW_CODE_REVIEW_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_MAX_ITERS" + [ -n "$REVIEW_CODE_REVIEW_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_MAX_LLM_CALLS" + [ -n "$REVIEW_CODE_REVIEW_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_REASONING_EFFORT" + [ -n "$REVIEW_CODE_REVIEW_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_TEMPERATURE" + + # Per-signature max_tokens + [ -n "$REVIEW_SCOPE_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SCOPE_MAX_TOKENS" + [ -n "$REVIEW_CODE_REVIEW_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_CODE_REVIEW_MAX_TOKENS" + [ -n "$REVIEW_DOC_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_MAX_TOKENS" + [ -n "$REVIEW_SUPPLY_CHAIN_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_MAX_TOKENS" + [ -n "$REVIEW_SUMMARY_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_MAX_TOKENS" + [ -n "$REVIEW_AUDIT_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_MAX_TOKENS" + + # Memory module max_tokens + [ -n "$MEMORY_DISTILLER_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_MAX_TOKENS" + [ -n "$MEMORY_CARTOGRAPHER_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_MAX_TOKENS" + # Doc review - [ -n "$DOC_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_ENABLED" - [ -n "$DOC_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MODEL" - [ -n "$DOC_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_REASONING_EFFORT" - [ -n "$DOC_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_TEMPERATURE" - + [ -n "$REVIEW_DOC_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_ENABLED" + [ -n "$REVIEW_DOC_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_MODEL" + [ -n "$REVIEW_DOC_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_MAX_ITERS" + [ -n "$REVIEW_DOC_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_MAX_LLM_CALLS" + [ -n "$REVIEW_DOC_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_REASONING_EFFORT" + [ -n "$REVIEW_DOC_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_DOC_TEMPERATURE" + # Supply chain - [ -n "$SUPPLY_CHAIN_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_ENABLED" - [ -n "$SUPPLY_CHAIN_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MODEL" - [ -n "$SUPPLY_CHAIN_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MAX_ITERS" - [ -n "$SUPPLY_CHAIN_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MAX_LLM_CALLS" - [ -n "$SUPPLY_CHAIN_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_REASONING_EFFORT" - [ -n "$SUPPLY_CHAIN_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_TEMPERATURE" - [ -n "$SUPPLY_CHAIN_SCAN_UNCHANGED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_SCAN_UNCHANGED" - + [ -n "$REVIEW_SUPPLY_CHAIN_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_ENABLED" + [ -n "$REVIEW_SUPPLY_CHAIN_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_MODEL" + [ -n "$REVIEW_SUPPLY_CHAIN_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_MAX_ITERS" + [ -n "$REVIEW_SUPPLY_CHAIN_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_MAX_LLM_CALLS" + [ -n "$REVIEW_SUPPLY_CHAIN_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_REASONING_EFFORT" + [ -n "$REVIEW_SUPPLY_CHAIN_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_TEMPERATURE" + [ -n "$REVIEW_SUPPLY_CHAIN_SCAN_UNCHANGED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUPPLY_CHAIN_SCAN_UNCHANGED" + # Summary - [ -n "$SUMMARY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_ENABLED" - [ -n "$SUMMARY_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MAX_ITERS" - [ -n "$SUMMARY_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MODEL" - [ -n "$SUMMARY_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MAX_LLM_CALLS" - [ -n "$SUMMARY_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_REASONING_EFFORT" - [ -n "$SUMMARY_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_TEMPERATURE" + [ -n "$REVIEW_SUMMARY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_ENABLED" + [ -n "$REVIEW_SUMMARY_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_MAX_ITERS" + [ -n "$REVIEW_SUMMARY_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_MODEL" + [ -n "$REVIEW_SUMMARY_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_MAX_LLM_CALLS" + [ -n "$REVIEW_SUMMARY_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_REASONING_EFFORT" + [ -n "$REVIEW_SUMMARY_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_SUMMARY_TEMPERATURE" # Audit - [ -n "$AUDIT_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_ENABLED" - [ -n "$AUDIT_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_MODEL" - [ -n "$AUDIT_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_MAX_ITERS" - [ -n "$AUDIT_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_MAX_LLM_CALLS" - [ -n "$AUDIT_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_REASONING_EFFORT" - [ -n "$AUDIT_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_TEMPERATURE" + [ -n "$REVIEW_AUDIT_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_ENABLED" + [ -n "$REVIEW_AUDIT_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_MODEL" + [ -n "$REVIEW_AUDIT_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_MAX_ITERS" + [ -n "$REVIEW_AUDIT_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_MAX_LLM_CALLS" + [ -n "$REVIEW_AUDIT_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_REASONING_EFFORT" + [ -n "$REVIEW_AUDIT_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e REVIEW_AUDIT_TEMPERATURE" - # Other settings - [ -n "$EXCLUDED_DIRECTORIES" ] && DOCKER_ARGS="$DOCKER_ARGS -e EXCLUDED_DIRECTORIES" + # Output settings (REVIEW_OUTPUT_FORMAT removed - only via CLI flag) # Memory (Hippocampus) - [ -n "$MEMORY_DEFAULT_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DEFAULT_ENABLED" + [ -n "$MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_ENABLED" [ -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_DISTILLER_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_MODEL" [ -n "$MEMORY_DISTILLER_REASONING_EFFORT" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_REASONING_EFFORT" @@ -667,12 +759,28 @@ runs: [ -n "$MEMORY_CARTOGRAPHER_TEMPERATURE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_TEMPERATURE" [ -n "$MEMORY_CARTOGRAPHER_MAX_ITERS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_MAX_ITERS" [ -n "$MEMORY_CARTOGRAPHER_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_MAX_LLM_CALLS" - [ -n "$SCOPE_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MEMORY_ENABLED" - [ -n "$CODE_REVIEW_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e CODE_REVIEW_MEMORY_ENABLED" - [ -n "$DOC_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e DOC_MEMORY_ENABLED" - [ -n "$SUPPLY_CHAIN_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUPPLY_CHAIN_MEMORY_ENABLED" - [ -n "$SUMMARY_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SUMMARY_MEMORY_ENABLED" - [ -n "$AUDIT_MEMORY_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e AUDIT_MEMORY_ENABLED" + [ -n "$MEMORY_RETAIN_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RETAIN_MODEL" + [ -n "$MEMORY_CONSOLIDATION_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CONSOLIDATION_MODEL" + [ -n "$MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE" + [ -n "$MEMORY_COMPACT_TRAJECTORY" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_COMPACT_TRAJECTORY" + [ -n "$MEMORY_MAX_HIPPOCAMPUS_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MAX_HIPPOCAMPUS_TOKENS" + [ -n "$MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS" + [ -n "$MEMORY_MAX_TRAJECTORY_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MAX_TRAJECTORY_TOKENS" + [ -n "$MEMORY_MAX_QUESTION_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MAX_QUESTION_TOKENS" + [ -n "$MEMORY_DISTILLER_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_DISTILLER_MAX_TOKENS" + [ -n "$MEMORY_CARTOGRAPHER_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_CARTOGRAPHER_MAX_TOKENS" + [ -n "$MEMORY_MENTAL_MODELS_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MENTAL_MODELS_MODEL" + [ -n "$MEMORY_MENTAL_MODELS_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MENTAL_MODELS_MAX_TOKENS" + [ -n "$MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS" + [ -n "$MEMORY_EMBEDDINGS_MAX_INPUT_CHARS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_EMBEDDINGS_MAX_INPUT_CHARS" + [ -n "$MEMORY_PREFRONTAL_REFLECTS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_PREFRONTAL_REFLECTS" + [ -n "$MEMORY_RECALL_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RECALL_MODEL" + [ -n "$MEMORY_RECALL_REACH" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RECALL_REACH" + [ -n "$MEMORY_RECALL_MAX_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RECALL_MAX_TOKENS" + [ -n "$MEMORY_RECALL_MAX_TOOL_TOKENS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RECALL_MAX_TOOL_TOKENS" + [ -n "$MEMORY_RECALL_MAX_TOOL_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RECALL_MAX_TOOL_CALLS" + [ -n "$MEMORY_EMBEDDINGS_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_EMBEDDINGS_MODEL" + [ -n "$MEMORY_RETAIN_CHUNK_SIZE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MEMORY_RETAIN_CHUNK_SIZE" # Build codespy command arguments CMD_ARGS="review ${{ steps.pr-url.outputs.url }}" diff --git a/codespy.yaml b/codespy.yaml index eafe6e0..3e29285 100644 --- a/codespy.yaml +++ b/codespy.yaml @@ -1,13 +1,29 @@ # codespy.yaml - Configuration file for codespy # # ENVIRONMENT VARIABLE OVERRIDES: -# All settings can be overridden via environment variables. -# - Flat values: (e.g., OUTPUT_FORMAT=json, DEFAULT_MODEL=openai/gpt-5) -# - Signature values: _ -# Examples: -# CODE_REVIEW_MAX_ITERS=15 -# CODE_REVIEW_ENABLED=false -# SUPPLY_CHAIN_MODEL=openai/gpt-4-turbo +# All settings can be overridden via environment variables using the YAML path, +# uppercased and joined with underscores. The llm., memory.hippocampus., +# memory.cerebral. and memory.prefrontal. segments are dropped; llm.retries, +# llm.timeout and memory.prefrontal.reflects keep their full names. +# Examples: +# - llm.default_model → DEFAULT_MODEL +# - llm.retries → LLM_RETRIES +# - review.code_review.max_iters → REVIEW_CODE_REVIEW_MAX_ITERS +# - memory.hippocampus.distiller.model → MEMORY_DISTILLER_MODEL +# - memory.cerebral.retain.model → MEMORY_RETAIN_MODEL +# - memory.cerebral.consolidation.model → MEMORY_CONSOLIDATION_MODEL +# - memory.cerebral.mental_models.model → MEMORY_MENTAL_MODELS_MODEL +# - memory.cerebral.mental_models.max_tokens → MEMORY_MENTAL_MODELS_MAX_TOKENS +# - memory.cerebral.mental_models.min_refresh_seconds → MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS +# - memory.prefrontal.reflects → MEMORY_PREFRONTAL_REFLECTS +# - memory.prefrontal.recall.model → MEMORY_RECALL_MODEL +# - memory.prefrontal.recall.reach → MEMORY_RECALL_REACH +# - memory.prefrontal.recall.max_tokens → MEMORY_RECALL_MAX_TOKENS +# - memory.prefrontal.recall.max_tool_tokens → MEMORY_RECALL_MAX_TOOL_TOKENS +# - memory.prefrontal.recall.max_tool_calls → MEMORY_RECALL_MAX_TOOL_CALLS +# +# Provider credentials keep their standard names: +# OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, AWS_*, AZURE_* # # Priority: Environment Variables > YAML Config > Defaults @@ -18,340 +34,224 @@ llm: # OpenAI openai_api_key: null # OPENAI_API_KEY openai_api_base: null # OPENAI_API_BASE - auto_discover_openai: true # AUTO_DISCOVER_OPENAI (set to false to disable) + auto_discover_openai: true # AUTO_DISCOVER_OPENAI # Anthropic anthropic_api_key: null # ANTHROPIC_API_KEY - auto_discover_anthropic: true # AUTO_DISCOVER_ANTHROPIC (set to false to disable) + auto_discover_anthropic: true # AUTO_DISCOVER_ANTHROPIC # Google Gemini gemini_api_key: null # GEMINI_API_KEY - auto_discover_gemini: true # AUTO_DISCOVER_GEMINI (set to false to disable) + auto_discover_gemini: true # AUTO_DISCOVER_GEMINI # AWS Bedrock aws_region: us-east-1 # AWS_REGION aws_access_key_id: null # AWS_ACCESS_KEY_ID aws_secret_access_key: null # AWS_SECRET_ACCESS_KEY aws_profile: null # AWS_PROFILE - auto_discover_aws: true # AUTO_DISCOVER_AWS (set to false to disable) + auto_discover_aws: true # AUTO_DISCOVER_AWS # Azure OpenAI azure_api_key: null # AZURE_API_KEY azure_api_base: null # AZURE_API_BASE azure_api_version: null # AZURE_API_VERSION - # Enable provider-side prompt caching (Anthropic, OpenAI, Bedrock, etc.) - # Caches system prompts on the LLM provider's servers to reduce latency and costs - enable_prompt_caching: true # ENABLE_PROMPT_CACHING - -# Minimum confidence threshold for reported issues (0.0–1.0). -# 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 - + # Default model settings + default_model: bedrock/converse/nvidia.nemotron-nano-3-30b # DEFAULT_MODEL + extraction_model: bedrock/converse/nvidia.nemotron-nano-3-30b # EXTRACTION_MODEL (falls back to default_model; also used by Cerebral two-step structured output for memory models litellm cannot schema-enforce) + default_max_iters: 5 # DEFAULT_MAX_ITERS + default_max_llm_calls: 8 # DEFAULT_MAX_LLM_CALLS + default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT + default_temperature: 1.0 # DEFAULT_TEMPERATURE + default_max_tokens: 32000 # DEFAULT_MAX_TOKENS + + # Global LLM reliability settings + retries: 2 # LLM_RETRIES + timeout: 240 # LLM_TIMEOUT + + # RLM Fallback (context rot prevention) + rlm_fallback: + enabled: true # RLM_FALLBACK_ENABLED + react_threshold: 0.30 # RLM_FALLBACK_REACT_THRESHOLD + chain_of_thought_threshold: 0.40 # RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD + predict_threshold: 0.50 # RLM_FALLBACK_PREDICT_THRESHOLD # ============================================================================ # GIT PLATFORMS # ============================================================================ github: token: null # GITHUB_TOKEN (auto-discovered if not set) - auto_discover_token: true # GITHUB_AUTO_DISCOVER_TOKEN (set to false to disable auto-discovery) + auto_discover_token: true # GITHUB_AUTO_DISCOVER_TOKEN gitlab: token: null # GITLAB_TOKEN (auto-discovered if not set) - url: https://gitlab.com # GITLAB_URL (for self-hosted instances) - auto_discover_token: true # GITLAB_AUTO_DISCOVER_TOKEN (set to false to disable auto-discovery) + url: https://gitlab.com # GITLAB_URL + auto_discover_token: true # GITLAB_AUTO_DISCOVER_TOKEN # ============================================================================ # MEMORY # ============================================================================ -# Hippocampus lets scope-based agents (scope, code_review, doc, supply_chain) -# and the summary step consolidate their run into a ContextMemory and -# persist it as an Episode. Save-only for now (no loading). Disabled by -# default globally; enabled by default for summary — see `memory:` -# blocks under each signature below. - -# Episodes are persisted in PostgreSQL. When postgres.host is unset, -# pg0-embedded auto-starts a local PostgreSQL instance (zero config). -# For production, set postgres.host to an external PostgreSQL (AWS RDS, etc.). memory: - # External PostgreSQL (production, AWS RDS, etc.). - # When postgres.host is unset, pg0-embedded auto-starts a local instance. + # PostgreSQL connection postgres: host: null # MEMORY_POSTGRES_HOST port: 5432 # MEMORY_POSTGRES_PORT - user: null # MEMORY_POSTGRES_USER (default: postgres) + user: null # MEMORY_POSTGRES_USER password: null # MEMORY_POSTGRES_PASSWORD database: codespy # MEMORY_POSTGRES_DATABASE - 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-embedded settings 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 - - # Memory default — overridable per-signature via signatures..memory - default_enabled: false # MEMORY_DEFAULT_ENABLED - - # Four independent token budgets, from most to least cost-sensitive: - # - # 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 / 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 - # corrupt an exact constant). Lower it for more, terser items; raise it for - # fewer, richer ones. - # 3. max_trajectory_tokens — cap on the trajectory fed to the Distiller. Tool-using - # agents can produce 100k+ token trajectories and TwoStepAdapter sends the value - # twice, so keep this to ~5-15% of the reflection model's context window. - # null = full trajectory (unbounded — not recommended for tool-using agents). - # 4. max_question_tokens — cap on the serialized agent inputs used as the reflection - # "question". Without it, every input field is sent in full (for code review that - # means the complete patch of every changed file). null = unbounded. - max_context_memory_tokens: 16384 # MEMORY_MAX_CONTEXT_MEMORY_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 - - # LLM settings for the reflection modules. The Distiller summarizes a - # trajectory; the Cartographer curates the context memory. Both are compact - # summarize/curate tasks, so a cheaper tier than code review usually suffices. - # null inherits the top-level default_* value. - distiller: - model: null # MEMORY_DISTILLER_MODEL - reasoning_effort: null # MEMORY_DISTILLER_REASONING_EFFORT - temperature: null # MEMORY_DISTILLER_TEMPERATURE - max_tokens: null # MEMORY_DISTILLER_MAX_TOKENS - max_iters: 1 # MEMORY_DISTILLER_MAX_ITERS - max_llm_calls: 2 # MEMORY_DISTILLER_MAX_LLM_CALLS - cartographer: - model: null # MEMORY_CARTOGRAPHER_MODEL - reasoning_effort: null # MEMORY_CARTOGRAPHER_REASONING_EFFORT - temperature: null # MEMORY_CARTOGRAPHER_TEMPERATURE - max_tokens: null # MEMORY_CARTOGRAPHER_MAX_TOKENS - max_iters: 1 # MEMORY_CARTOGRAPHER_MAX_ITERS - max_llm_calls: 2 # MEMORY_CARTOGRAPHER_MAX_LLM_CALLS - + port: null # MEMORY_PG0_PORT + data_dir: null # MEMORY_PG0_DATA_DIR + + # Master switch + enabled: false # MEMORY_ENABLED + bank_id: codebase # MEMORY_BANK_ID + + # Hippocampus (episodic memory) + hippocampus: + compact_trajectory: true # MEMORY_COMPACT_TRAJECTORY + max_hippocampus_tokens: 16384 # MEMORY_MAX_HIPPOCAMPUS_TOKENS + max_hippocampus_item_tokens: 512 # MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS + max_trajectory_tokens: 16384 # MEMORY_MAX_TRAJECTORY_TOKENS + max_question_tokens: 8192 # MEMORY_MAX_QUESTION_TOKENS + distiller: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_DISTILLER_MODEL + reasoning_effort: null # MEMORY_DISTILLER_REASONING_EFFORT + temperature: null # MEMORY_DISTILLER_TEMPERATURE + max_tokens: null # MEMORY_DISTILLER_MAX_TOKENS + max_iters: 1 # MEMORY_DISTILLER_MAX_ITERS + max_llm_calls: 2 # MEMORY_DISTILLER_MAX_LLM_CALLS + cartographer: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_CARTOGRAPHER_MODEL + reasoning_effort: null # MEMORY_CARTOGRAPHER_REASONING_EFFORT + temperature: null # MEMORY_CARTOGRAPHER_TEMPERATURE + max_tokens: null # MEMORY_CARTOGRAPHER_MAX_TOKENS + max_iters: 1 # MEMORY_CARTOGRAPHER_MAX_ITERS + max_llm_calls: 2 # MEMORY_CARTOGRAPHER_MAX_LLM_CALLS + + # Cerebral (semantic memory) + cerebral: + retain: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_RETAIN_MODEL (falls back to llm.default_model) + chunk_size: 12288 # MEMORY_RETAIN_CHUNK_SIZE (chars, must be < 64000) + consolidation: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_CONSOLIDATION_MODEL (falls back to retain model → default) + max_observations_per_scope: 100 # MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE (-1 = unlimited, 0 = no new observations) + mental_models: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_MENTAL_MODELS_MODEL (briefing refresh; falls back to prefrontal model → retain → default) + max_tokens: 2048 # MEMORY_MENTAL_MODELS_MAX_TOKENS (briefing length) + min_refresh_seconds: 0 # MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS (0 = refresh after every consolidation; N = at most one automatic briefing refresh per N seconds, nothing lost thanks to delta) + embeddings: + model: bedrock/cohere.embed-v4:0 # MEMORY_EMBEDDINGS_MODEL (auto-derived from provider) + max_input_chars: null # MEMORY_EMBEDDINGS_MAX_INPUT_CHARS (null=auto per model, e.g. 2048 for Bedrock Cohere v3; 0=off) + + # Prefrontal (semantic recall from Cerebral into agent context: read-only) + prefrontal: + reflects: 3 # MEMORY_PREFRONTAL_REFLECTS (reflect iterations for recall and briefings; 0 = raw-fact recall, no LLM, no briefings) + recall: + model: bedrock/converse/nvidia.nemotron-super-3-120b # MEMORY_RECALL_MODEL (reflect; also briefings fallback unless cerebral.mental_models.model set) + reach: org # MEMORY_RECALL_REACH (local | org | bank) + max_tokens: 8192 # MEMORY_RECALL_MAX_TOKENS (pre-call context budget) + max_tool_tokens: 2048 # MEMORY_RECALL_MAX_TOOL_TOKENS (tool result budget) + max_tool_calls: 0 # MEMORY_RECALL_MAX_TOOL_CALLS (0 = recall_memory tool off) # ============================================================================ -# RLM FALLBACK (CONTEXT ROT PREVENTION) +# REVIEW # ============================================================================ -# Proactive fallback to RLM when input tokens exceed a ratio of the model's -# max_input_tokens. Prevents quality degradation that occurs well before the -# hard context window limit. The hard overflow safety net remains active -# regardless of these settings. -# -# Thresholds are per DSPy module type: -# - react: Multi-step tool-using agents (code_review, scope, supply_chain) -# - chain_of_thought: Single-pass reasoning (doc, summary, audit) -# - predict: Basic completion (not currently used) -# -# Minimum floor: proactive threshold never fires below 8192 input tokens. - -rlm_fallback: - enabled: true # RLM_FALLBACK_ENABLED - react_threshold: 0.30 # RLM_FALLBACK_REACT_THRESHOLD - chain_of_thought_threshold: 0.40 # RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD - predict_threshold: 0.50 # RLM_FALLBACK_PREDICT_THRESHOLD - - -# ============================================================================ -# SIGNATURES -# ============================================================================ - -# Each signature config supports: enabled, max_iters, model, reasoning_effort, -# temperature, max_tokens. Set to null to inherit the corresponding default_* value. - -# -# RECOMMENDED MODEL STRATEGY -# ============================================================================ -# codespy uses a tiered model approach to balance review quality and cost: -# -# Smart (default_model): Used for core analysis — code review (bugs, security, -# smells), supply chain, scope identification, doc review. Requires strong reasoning. -# Recommended: anthropic/claude-opus-4-6 or equivalent. -# -# Mid-tier (extraction_model): Used for structured field -# extraction (TwoStepAdapter). Needs accuracy but not deep reasoning. -# Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. -# -# Cheap (summary): Used for PR summary generation. Simple synthesis -# task. Recommended: anthropic/claude-haiku-4-5-20251001 or equivalent. -# - # Mid-tier (memory.distiller / memory.cartographer): Memory reflection — - # summarizing a trajectory and curating the context memory. Requires nuance. - # Recommended: anthropic/claude-sonnet-4-5-20250929 or equivalent. -# -# By default, all models fall back to default_model. Override extraction_model, -# the summary model, and the reflection models for cost optimization: -# -# default_model: anthropic/claude-opus-4-6 -# extraction_model: anthropic/claude-sonnet-4-5-20250929 -# signatures: -# summary: -# model: anthropic/claude-haiku-4-5-20251001 -# memory: -# distiller: -# model: anthropic/claude-sonnet-4-5-20250929 -# cartographer: -# model: anthropic/claude-sonnet-4-5-20250929 -# ============================================================================ - - -# These apply to all signatures and reflection modules unless overridden -default_model: anthropic/claude-opus-4-6 # DEFAULT_MODEL -extraction_model: null # EXTRACTION_MODEL (falls back to default_model) -default_max_iters: 5 # DEFAULT_MAX_ITERS -default_max_llm_calls: 8 # DEFAULT_MAX_LLM_CALLS -default_reasoning_effort: medium # DEFAULT_REASONING_EFFORT (minimal | low | medium | high) -default_temperature: 0.2 # DEFAULT_TEMPERATURE - -# Output token budget for a single completion. This is an OUTPUT ceiling, not a -# context window, and reasoning/thinking tokens are charged against it — so it -# must be well above the expected answer size whenever reasoning_effort is set. -# Leaving it unset is not an option: LiteLLM then silently falls back to its own -# 4096 default, which truncates responses mid-answer (DSPy warns -# "LM response was truncated ... max_tokens=None"). -# The value is clamped down to each model's real output ceiling before use, so a -# generous setting stays valid on smaller models. Models LiteLLM doesn't know -# (Ollama, custom endpoints) receive it unchanged. -default_max_tokens: 32000 # DEFAULT_MAX_TOKENS - - -# Global LLM reliability settings -llm_retries: 2 # LLM_RETRIES (number of retries for LLM API calls) -llm_timeout: 240 # LLM_TIMEOUT (timeout in seconds for LLM calls) - -signatures: - # Supply Chain Security signature +review: + min_confidence: 0.81 # REVIEW_MIN_CONFIDENCE + output_format: markdown # REVIEW_OUTPUT_FORMAT + output_stdout: true # REVIEW_OUTPUT_STDOUT + output_git: true # REVIEW_OUTPUT_GIT + cache_dir: ~/.cache/codespy # REVIEW_CACHE_DIR + + excluded_directories: # REVIEW_EXCLUDED_DIRECTORIES (JSON array in env) + - vendor + - node_modules + - third_party + - external + - deps + - _vendor + - vendored + - dist + - build + - out + - target + - .bundle + - Pods + - Carthage + - bower_components + - jspm_packages + - .git + - .svn + - .hg + - __pycache__ + - .cache + - .pytest_cache + - .mypy_cache + - .ruff_cache + + # Supply Chain Security supply_chain: - enabled: true # SUPPLY_CHAIN_ENABLED - max_iters: null # SUPPLY_CHAIN_MAX_ITERS - max_llm_calls: null # SUPPLY_CHAIN_MAX_LLM_CALLS - model: null # SUPPLY_CHAIN_MODEL (Haiku 4.5) - reasoning_effort: null # SUPPLY_CHAIN_REASONING_EFFORT - temperature: null # SUPPLY_CHAIN_TEMPERATURE - max_tokens: null # SUPPLY_CHAIN_MAX_TOKENS - scan_unchanged: false # SUPPLY_CHAIN_SCAN_UNCHANGED - # When true: scans ALL artifacts (Dockerfiles, etc.) and manifests - # When false (default): only scans artifacts/manifests that were modified in the MR - memory: - enabled: null # SUPPLY_CHAIN_MEMORY_ENABLED (null -> memory.default_enabled) - - # Code Reviewer signature (bugs, security, removed defensive code, code smells) - # Unified code review: bugs, security vulnerabilities, and code smells in a single agent pass per scope + enabled: true # REVIEW_SUPPLY_CHAIN_ENABLED + max_iters: null # REVIEW_SUPPLY_CHAIN_MAX_ITERS + max_llm_calls: null # REVIEW_SUPPLY_CHAIN_MAX_LLM_CALLS + model: bedrock/converse/nvidia.nemotron-super-3-120b # REVIEW_SUPPLY_CHAIN_MODEL + reasoning_effort: null # REVIEW_SUPPLY_CHAIN_REASONING_EFFORT + temperature: null # REVIEW_SUPPLY_CHAIN_TEMPERATURE + max_tokens: null # REVIEW_SUPPLY_CHAIN_MAX_TOKENS + scan_unchanged: false # REVIEW_SUPPLY_CHAIN_SCAN_UNCHANGED + + # Code Review code_review: - enabled: true # CODE_REVIEW_ENABLED - 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 - max_tokens: null # CODE_REVIEW_MAX_TOKENS - memory: - enabled: null # CODE_REVIEW_MEMORY_ENABLED (null -> memory.default_enabled) - - # Documentation Reviewer signature (compares patches against extracted documentation) - # Note: doc extraction is now deterministic (no LLM) — see doc_extractor.py + enabled: true # REVIEW_CODE_REVIEW_ENABLED + max_iters: 5 # REVIEW_CODE_REVIEW_MAX_ITERS + max_llm_calls: 8 # REVIEW_CODE_REVIEW_MAX_LLM_CALLS + model: bedrock/converse/global.anthropic.claude-opus-5-5 # REVIEW_CODE_REVIEW_MODEL + reasoning_effort: null # REVIEW_CODE_REVIEW_REASONING_EFFORT + temperature: null # REVIEW_CODE_REVIEW_TEMPERATURE + max_tokens: null # REVIEW_CODE_REVIEW_MAX_TOKENS + + # Documentation Review doc: - enabled: true # DOC_ENABLED - 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 - max_tokens: null # DOC_MAX_TOKENS - memory: - enabled: null # DOC_MEMORY_ENABLED (null -> memory.default_enabled) - - # Scope Identifier signature + enabled: true # REVIEW_DOC_ENABLED + max_iters: 2 # REVIEW_DOC_MAX_ITERS + max_llm_calls: 4 # REVIEW_DOC_MAX_LLM_CALLS + model: bedrock/converse/global.anthropic.claude-opus-5-5 # REVIEW_DOC_MODEL + reasoning_effort: null # REVIEW_DOC_REASONING_EFFORT + temperature: null # REVIEW_DOC_TEMPERATURE + max_tokens: null # REVIEW_DOC_MAX_TOKENS + + # Scope Identification scope: - enabled: true # SCOPE_ENABLED - 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 - max_tokens: null # SCOPE_MAX_TOKENS - memory: - enabled: null # SCOPE_MEMORY_ENABLED (null -> memory.default_enabled) - - # Summarizer signature + enabled: true # REVIEW_SCOPE_ENABLED + max_iters: 3 # REVIEW_SCOPE_MAX_ITERS + max_llm_calls: 5 # REVIEW_SCOPE_MAX_LLM_CALLS + model: bedrock/converse/global.anthropic.claude-opus-5-5 # REVIEW_SCOPE_MODEL + reasoning_effort: null # REVIEW_SCOPE_REASONING_EFFORT + temperature: null # REVIEW_SCOPE_TEMPERATURE + max_tokens: null # REVIEW_SCOPE_MAX_TOKENS + skip_refinement_when_clean: null # REVIEW_SCOPE_SKIP_REFINEMENT_WHEN_CLEAN + + # Summary summary: - enabled: true # SUMMARY_ENABLED - max_iters: 1 # SUMMARY_MAX_ITERS - max_llm_calls: 2 # SUMMARY_MAX_LLM_CALLS - model: null # SUMMARY_MODEL (falls back to default_model) - reasoning_effort: null # SUMMARY_REASONING_EFFORT - temperature: null # SUMMARY_TEMPERATURE - max_tokens: null # SUMMARY_MAX_TOKENS - memory: - enabled: true # SUMMARY_MEMORY_ENABLED (null -> memory.default_enabled) - - # Auditor signature (quality assessment + recommendation after all reviews) + enabled: true # REVIEW_SUMMARY_ENABLED + max_iters: 1 # REVIEW_SUMMARY_MAX_ITERS + max_llm_calls: 2 # REVIEW_SUMMARY_MAX_LLM_CALLS + model: bedrock/converse/nvidia.nemotron-super-3-120b # REVIEW_SUMMARY_MODEL + reasoning_effort: null # REVIEW_SUMMARY_REASONING_EFFORT + temperature: null # REVIEW_SUMMARY_TEMPERATURE + max_tokens: null # REVIEW_SUMMARY_MAX_TOKENS + + # Audit audit: - enabled: true # AUDIT_ENABLED - max_iters: 1 # AUDIT_MAX_ITERS - max_llm_calls: 2 # AUDIT_MAX_LLM_CALLS - model: null # AUDIT_MODEL - reasoning_effort: null # AUDIT_REASONING_EFFORT - temperature: null # AUDIT_TEMPERATURE - max_tokens: null # AUDIT_MAX_TOKENS - memory: - enabled: null # AUDIT_MEMORY_ENABLED (null -> memory.default_enabled) - -# ============================================================================ -# OUTPUT -# ============================================================================ -output_format: markdown # OUTPUT_FORMAT (markdown | json) -output_stdout: true # OUTPUT_STDOUT (enable stdout output) -output_git: true # OUTPUT_GIT (post review to GitHub PR or GitLab MR) -cache_dir: ~/.cache/codespy # CACHE_DIR - -# ============================================================================ -# FILE EXCLUSIONS -# ============================================================================ -# Directories to skip during code review (binary files, lock files, and -# minified files are always excluded automatically) -excluded_directories: # EXCLUDED_DIRECTORIES (JSON array in env) - # Vendor/dependency directories - - vendor - - node_modules - - third_party - - external - - deps - - _vendor - - vendored - # Build output directories - - dist - - build - - out - - target - # Package manager directories - - .bundle - - Pods - - Carthage - - bower_components - - jspm_packages - # Version control - - .git - - .svn - - .hg - # Cache directories - - __pycache__ - - .cache - - .pytest_cache - - .mypy_cache - - .ruff_cache + enabled: true # REVIEW_AUDIT_ENABLED + max_iters: 1 # REVIEW_AUDIT_MAX_ITERS + max_llm_calls: 2 # REVIEW_AUDIT_MAX_LLM_CALLS + model: bedrock/converse/global.anthropic.claude-opus-5-5 # REVIEW_AUDIT_MODEL + reasoning_effort: null # REVIEW_AUDIT_REASONING_EFFORT + temperature: null # REVIEW_AUDIT_TEMPERATURE + max_tokens: null # REVIEW_AUDIT_MAX_TOKENS diff --git a/docs/architecture.md b/docs/architecture.md index 514473f..f407a89 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -108,12 +108,23 @@ RLM puts inputs into a sandboxed Python interpreter rather than the LLM prompt. The model writes code to access and process variables (e.g., chunking large patches via `llm_query()`), eliminating context window limits entirely. -The composition order is: `Hippocampus(ContextSafe(Module))` — Hippocampus -handles memory, ContextSafe handles context quality, each with single responsibility. +The composition order is: `ContextSafe(Module)` — ContextSafe handles context quality. +Hippocampus is a separate component that agents call explicitly. -## Hippocampus Memory +## Memory -Episode-based memory that wraps DSPy agents with persistent context across reviews. Agents accumulate knowledge about a codebase scope over time — patterns, constants, parsing schemas, and reuse it in subsequent reviews of the same code area. +CodeSpy's memory system has three independent components: + +- **Hippocampus**: Episode-based memory that learns from the current review. Agents call + `Hippocampus()` to create an episode, `aobserve()` to feed results, and `end_episode()` + to run the Distiller/Cartographer reflection pipeline. Agents never receive Hippocampus + context memory directly; prior knowledge reaches them only through Cerebral → Prefrontal. + +- **Cerebral**: Retains Hippocampus episodes into Hindsight semantic memory, consolidates + observations, and refreshes mental models (briefings). + +- **Prefrontal**: Reads Cerebral before agent calls and injects a read-only `prefrontal_memory` + input (briefings, recalled facts). RLM agents also get a `recall_memory` tool. See [Memory System](memory.md) for implementation details. diff --git a/docs/configuration.md b/docs/configuration.md index 7bf818e..4fc6c98 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -106,13 +106,11 @@ AUTO_DISCOVER_GEMINI=false | Setting | Env Var | Default | Description | |---------|---------|---------|-------------| -| Model | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Primary model for all signatures | +| Model | `DEFAULT_MODEL` | `bedrock/converse/nvidia.nemotron-nano-3-30b` | Primary fallback model (most signatures have dedicated pins) | | Reasoning effort | `DEFAULT_REASONING_EFFORT` | `medium` | Provider reasoning budget: `minimal`, `low`, `medium`, `high` | | Max tokens | `DEFAULT_MAX_TOKENS` | `32000` | Output token budget per completion (reasoning tokens included) | -| Temperature | `DEFAULT_TEMPERATURE` | `0.2` | Default temperature for LLM calls | +| Temperature | `DEFAULT_TEMPERATURE` | `1.0` | 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 | @@ -120,16 +118,24 @@ AUTO_DISCOVER_GEMINI=false ## Recommended Model Strategy -| Tier | Role | Env Var | Default | Recommended | -|------|------|---------|---------|-------------| -| Smart | Core analysis & reasoning | `DEFAULT_MODEL` | `anthropic/claude-opus-4-6` | Claude Opus / GPT-5 | -| Mid-tier | Field extraction | `EXTRACTION_MODEL` | Falls back to DEFAULT_MODEL | Claude Sonnet | -| Cheap | PR summary | `SUMMARY_MODEL` | Falls back to DEFAULT_MODEL | Claude Haiku | -| Mid-tier | Memory reflection | `MEMORY_DISTILLER_MODEL` / `MEMORY_CARTOGRAPHER_MODEL` | Falls back to DEFAULT_MODEL | Claude Sonnet | +All models are now pinned in `codespy.yaml`. Setting a unit to `null` (YAML `model: null` or env `VAR=null`) re-enables the fallback chain. + +| Tier | Role | Env Var | Default | Fallback chain | +|------|------|---------|---------|----------------| +| Smart | Core analysis & reasoning | `REVIEW_*_MODEL` | `bedrock/converse/global.anthropic.claude-opus-5-5` | `model` → `DEFAULT_MODEL` | +| Mid-tier | Field extraction | `EXTRACTION_MODEL` | `bedrock/converse/nvidia.nemotron-nano-3-30b` | `DEFAULT_MODEL` | +| Cheap | PR summary | `REVIEW_SUMMARY_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `model` → `DEFAULT_MODEL` | +| Mid-tier | Memory reflection | `MEMORY_DISTILLER_MODEL` / `MEMORY_CARTOGRAPHER_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `model` → `DEFAULT_MODEL` | +| Mid-tier | Semantic memory (fact extraction) | `MEMORY_RETAIN_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `DEFAULT_MODEL` | +| Mid-tier | Semantic consolidation | `MEMORY_CONSOLIDATION_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `MEMORY_RETAIN_MODEL` → `DEFAULT_MODEL` | +| Mid-tier | Mental-model refresh (briefings) | `MEMORY_MENTAL_MODELS_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `MEMORY_RECALL_MODEL` → `MEMORY_RETAIN_MODEL` → `DEFAULT_MODEL` | +| Mid-tier | Semantic recall (reflect) | `MEMORY_RECALL_MODEL` | `bedrock/converse/nvidia.nemotron-super-3-120b` | `MEMORY_RETAIN_MODEL` → `DEFAULT_MODEL` | + +**Precedence:** Environment variables (`.env` or shell) > `codespy.yaml` (`github.token` / `gitlab.token`) > auto-discovery. ## Per-Signature Configuration -Each signature supports env var overrides: `_` +Each signature supports env var overrides: `REVIEW__` | Signature | Config Key | Available Settings | |-----------|------------|-------------------| @@ -140,7 +146,7 @@ Each signature supports env var overrides: `_` | 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` +Example: `REVIEW_CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929` ## Advanced Configuration (YAML) @@ -159,49 +165,54 @@ export DEFAULT_MODEL=anthropic/claude-opus-4-6 export DEFAULT_MAX_ITERS=20 # Per-signature settings (use signature name, not module name) -export CODE_REVIEW_MODEL=anthropic/claude-sonnet-4-5-20250929 +export REVIEW_CODE_REVIEW_MODEL=anthropic/claude_sonnet-4-5-20250929 # Output settings -export OUTPUT_STDOUT=false -export OUTPUT_GIT=true +export REVIEW_OUTPUT_STDOUT=false +export REVIEW_OUTPUT_GIT=true ``` ## Memory Configuration -Brief overview: +The memory system has three components: + +- **Hippocampus**: Episodic memory (learns from the current review) +- **Cerebral**: Semantic memory (retains episodes, consolidates, refreshes mental models) +- **Prefrontal**: Reads Cerebral and injects prior knowledge into agents + +Master switch and connection: | Setting | Env Var | Default | Description | |---------|---------|---------|-------------| +| Enabled | `MEMORY_ENABLED` | `false` | Enable memory globally | +| Bank ID | `MEMORY_BANK_ID` | `codebase` | Scopes all memory data | | 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` | `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) | -| pg0 data dir | `MEMORY_PG0_DATA_DIR` | — | pg0-embedded data directory (local dev) | -| Default enabled | `MEMORY_DEFAULT_ENABLED` | `false` | Enable memory globally | -| Context memory tokens | `MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | `16384` | Ceiling on persisted context memory | -| 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 | - -See [Memory System](memory.md) for full memory configuration details. +| PostgreSQL port | `MEMORY_POSTGRES_PORT` | `5432` | PostgreSQL port | +| PostgreSQL user | `MEMORY_POSTGRES_USER` | `postgres` | PostgreSQL user | +| PostgreSQL password | `MEMORY_POSTGRES_PASSWORD` | — | PostgreSQL password | +| PostgreSQL database | `MEMORY_POSTGRES_DATABASE` | `codespy` | PostgreSQL database | +| pg0 name | `MEMORY_PG0_NAME` | `codespy` | pg0-embedded database name | +| pg0 port | `MEMORY_PG0_PORT` | auto | pg0-embedded port | +| pg0 data dir | `MEMORY_PG0_DATA_DIR` | — | pg0-embedded data directory | + +See [Memory System](memory.md) for: +- Full configuration tables (token budgets, models, per-signature overrides) +- Database schema and SQL examples +- Lifecycle, data flow, and monitoring +- Prefrontal recall settings (reach, reflects, facets, `recall_memory` tool) ## Output Settings | Setting | Env Var | Default | Description | |---------|---------|---------|-------------| -| Format | `OUTPUT_FORMAT` | `markdown` | `markdown` or `json` | -| Stdout | `OUTPUT_STDOUT` | `true` | Enable stdout output | -| Git | `OUTPUT_GIT` | `true` | Post review to GitHub/GitLab | -| Cache dir | `CACHE_DIR` | `~/.cache/codespy` | Cache directory path | +| Format | `REVIEW_OUTPUT_FORMAT` | `markdown` | `markdown` or `json` | +| Stdout | `REVIEW_OUTPUT_STDOUT` | `true` | Enable stdout output | +| Git | `REVIEW_OUTPUT_GIT` | `true` | Post review to GitHub/GitLab | +| Cache dir | `REVIEW_CACHE_DIR` | `~/.cache/codespy` | Cache directory path | ## File Exclusions -`EXCLUDED_DIRECTORIES` (JSON array in env) — Directories to skip during code review. Binary files, lock files, and minified files are always excluded automatically. +`REVIEW_EXCLUDED_DIRECTORIES` (JSON array in env) — Directories to skip during code review. Binary files, lock files, and minified files are always excluded automatically. Default excluded directories: - Vendor/dependency: `vendor`, `node_modules`, `third_party`, `external`, `deps`, `_vendor`, `vendored` diff --git a/docs/memory.md b/docs/memory.md index 5fca36f..6eb43ba 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,21 +1,59 @@ [← Back to README](../README.md#documentation) -# Hippocampus Memory System +# Memory System -## Overview +CodeSpy's memory system has three independent components: -Episode-based memory that wraps DSPy agents with persistent context across reviews. -Agents accumulate knowledge about a codebase scope over time — patterns, constants, -parsing schemas — and reuse it in subsequent reviews of the same code area. +| Component | Role | Storage | Schema | +|-----------|------|---------|--------| +| **Hippocampus** | Learns from the current episode | PostgreSQL (`episodic`) | Episodic: episodes, observations, recalls | +| **Cerebral** | Retains episodes into semantic memory, consolidates, refreshes mental models | Hindsight bank | Semantic: observations, mental models | +| **Prefrontal** | Reads Cerebral and injects prior knowledge into agents | — | Read-only | -## Concepts +**Terminology note:** The module is "Prefrontal" but its configuration lives under `memory.prefrontal.recall.*` and its cost bucket is `memory_recall`. *Recall* = the read operation Prefrontal performs (config, bucket, table). + +## Lifecycle of one review run + +``` +1. forward() runs the review phase inside defer_episode_saves() + ├── Scope agent (if memory enabled and model refinement runs) + │ └── Own Prefrontal load (task="review", include_repo=True) + ├── Run-level Prefrontal recall (task="review", episode_id=NULL) + ├── Summary + ├── code_review, doc, supply_chain (concurrent) + └── Audit +2. Review published with "Memory: pending" (remote) or printed (local CLI/MCP) +3. finish_memory() starts queued saves + ├── Each episode: end_episode() → Distiller → Cartographer → evict → save + └── retain_episode() for each scope +4. join_episode_saves() +5. consolidate_run() → one consolidation over union of touched scopes +6. Hindsight refreshes briefings via refresh_after_consolidation trigger +7. Costs recollected, review edited with full costs (remote) or printed +``` + +If `forward()` raises, deferred saves still start but consolidation does not run. The at-exit hook finishes pending saves but does **not** call `consolidate_run`. + +## Agents and memory + +| Agent | Gets pre-call memory from | Has `recall_memory` tool | Writes episode | +|-------|---------------------------|--------------------------|----------------| +| scope | Own load (run-level if no refinement) | Yes | Yes | +| summary | Run-level | No | Yes | +| code_review | Run-level | Yes | Yes | +| doc | Run-level | No | Yes | +| supply_chain | Run-level | Yes | Yes | +| audit | Run-level | No | Yes | + +The run-level recall (`task="review"`, `episode_id=NULL`) covers all project scopes plus the repo. Only the scope agent loads its own memory when model refinement runs; all other agents share the run-level recall. + +## Hippocampus (episodic) ### Banks -A bank is the top-level data partition. All episodes, topics, observations, -and artifacts cascade-delete from a bank. +A bank is the top-level data partition. All episodes, topics, observations, artifacts and recalls cascade-delete from a bank. -- Configured via `MEMORY_BANK_ID` (default: `codespy`) +- Configured via `MEMORY_BANK_ID` (default: `codebase`) - Typical values: service name, team name, org identifier - Changing the bank ID starts a fresh memory silo — no data carries over @@ -27,13 +65,23 @@ and artifacts cascade-delete from a bank. ### Episodes -- 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_HOST` is set. -- `EpisodeStore.load_context(task, topic_ids, topic_prefix)` retrieves - the latest context by `timestamp DESC`, filtering on bank + task + topic +An Episode captures one agent's run: + +- `id` (UUID): unique episode identifier +- `run_id` (varchar): parent run identifier +- `task` (varchar): agent task name (e.g., "code_review") +- `module` (varchar): module identifier +- `question` (text): the task question +- `context_memory`: six sections of observations +- `mutations`: ADD/REPLACE/DELETE operations produced by Cartographer +- `artifacts`: named outputs (e.g., review markdown) +- `recalls`: Prefrontal loads and tool calls +- `timestamp`: episode timestamp +- `topics`: list of Topic objects for this episode -### Context Memory +`EpisodeStore.load_context(task, topic_ids)` merges every live (non-tombstone) observation from **all** prior episodes of the task that match the topics, ordered **oldest first** (`timestamp ASC`) so eviction drops the oldest facts on ties. + +### Context Memory sections Six sections (from general to specific): @@ -44,33 +92,185 @@ 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 `Observation` objects with `id`, `content`, and `topic_ids` linking -the observation 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 observation, a new -observation row is inserted with an incremented `version` number. +Observations are the versioned audit trail of context memory items in the database. Each time the Cartographer ADDs, REPLACEs, or DELETEs an observation, or eviction drops one, 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` +- `content` holds the new value (`NULL` for DELETE and EVICT tombstones) +- `previous_content` preserves the prior state (for REPLACE / DELETE / EVICT) +- `op_type` records the operation: `ADD`, `REPLACE`, `DELETE` or `EVICT` - Linked to the episode that produced the mutation and the topics it belongs to -Observations in `ContextMemory` map 1:1 to the latest non-deleted observation version. +Observations in `ContextMemory` map 1:1 to the latest observation version that is not a tombstone. ### 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`. +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 +### Reflection Pipeline -Auto-created by `EpisodeStore.ensure_schema()` on first connect. -Source: `src/codespy/agents/memory/postgres.py:61-211`. +At `end_episode()` (one pass per episode, after the review is published): + +1. **Distiller** — Analyzes the agent's trajectory (head 60% + tail 40% when `compact_trajectory=true`, capped at `max_trajectory_tokens`) and proposes `CacheCandidate` observations +2. **Cartographer** — Takes candidates + current context memory, decides operations: `ADD`, `REPLACE`, `DELETE` +3. **Eviction** — If memory exceeds `max_hippocampus_tokens`, evicts by: + - Section priority first: `parsing_schema` → `actions` → `reusable_results` → `domain_constants` → `context_roadmap` → `context_understanding` + - Then by score (helpful +1, harmful/stale −1, new +1) + - Then by age (oldest first) + +The Distiller tags each existing observation: + +- **`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. + +## Cerebral (semantic store) + +Cerebral retains Hippocampus episodes into Hindsight semantic memory and keeps mental models (briefings) fresh. + +### Tags and observation scopes + +Every retained item carries its `project_scope:` topic tags, `task:`, and `repo:`/`org:`. `pull_request:`, `episode:`, and `run_id:` tags are no longer added. + +Consolidation uses an explicit `observation_scopes` list: one `[org:, repo:, project_scope:]` scope per `project_scope` topic of the episode, or `[org:, repo:, project_scope:]` (the repo-root scope) when there is none. + +**Per-scope observation cap**: Hindsight supports `observation_scope_limits` to cap observations per scope. codespy sets one rule: `[org:*, repo:*, project_scope:*]` with limit `max_observations_per_scope` (default 100, -1 = unlimited, 0 = no new observations). Scopes that hit the cap only allow UPDATE/DELETE; nothing is trimmed. + +**Consolidation**: Runs once per run, over the union of touched scopes, on the pipeline thread after `join_episode_saves()`. Not inline in `retain_episode`. + +### Mental models (briefings) + +Mental models are the abstraction layer ("when X happens, consider Y because Z"). Cerebral keeps one per observation scope of each episode, plus one per repo (`[org:, repo:]`), when `memory.prefrontal.reflects > 0`: + +- id: `mm-` + first 32 hex chars of `sha1("|".join(sorted(tags)))` +- name: `Briefing: ` +- source query: *"What should an agent starting work here know so it does not rediscover it..."* +- `max_tokens = cerebral.mental_models.max_tokens`, trigger `refresh_after_consolidation` + +codespy creates a missing briefing with placeholder content `"Generating content..."` in `_sync_briefing_triggers`, before retain. The content comes from Hindsight's `refresh_after_consolidation` trigger, which fires during `consolidate_run` at the end of the same run. It also updates the trigger of an existing briefing when settings changed. + +#### Trigger configuration + +Briefing triggers are configured automatically with these settings: + +| Trigger Field | Value | Description | +|-------------|-------|-------------| +| `mode` | `delta` | Only new facts since last refresh (with structured delta-ops call) | +| `exclude_mental_models` | `true` | Briefings exclude other briefings | +| `include_chunks` | `false` | Observation text only (no file chunks) | +| `reflect_search_observations_max_tokens` | `3000` | Down from 5000 default | +| `reflect_search_observations_include_entities` | `false` | Exclude entity metadata | +| `min_refresh_interval_seconds` | `0` | Configurable via `MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS` | + +**Delta mode**: After the first full refresh, subsequent refreshes only process new facts since `last_memory_seen_at`. Falls back to full refresh when content is the placeholder or when the source query changed. + +**Deferral**: When `min_refresh_interval_seconds > 0`, automatic refreshes inside the window are deferred (operation marked `cancelled`, not `failed`). The next consolidation after the interval expires covers everything since `last_memory_seen_at`. + +Monitor briefing quality in the `memories` section of the review output and the `mental_models` content. + +### Hindsight maintenance + +- Orphaned pending operations are repaired by `orphans.py` +- Maintenance routines are repaired by `routines.py` + +## Prefrontal (recall) + +Prefrontal reads Cerebral and injects prior knowledge into agents. + +### Run-level load vs scope load + +**One run-level load** (`task="review"`, all project scopes + repo) shared by summary, code_review, doc, supply_chain, audit. Only the **scope** agent does its own load when model refinement runs. The other agents create a per-agent Prefrontal only for the `recall_memory` tool. + +### Facets + +Five facets answer different questions: + +| Facet | Question | Runs reflect? | Share | +|-------|----------|---------------|-------| +| `context` | What happened around this work? | Yes | 30% | +| `seen_before` | Have we met this before? | No (raw) | 20% | +| `decisions` | What was decided about it? | No (raw) | 20% | +| `patterns` | What keeps recurring? | No (raw) | 20% | +| `belief_changes` | What did we believe before, and what changed? | History only | 10% | + +Only the `context` facet runs the reflect loop; the others stay raw recall at MID budget. Facts are deduplicated across sections by id, then by exact text. + +### Reach + +`memory.prefrontal.recall.reach` (`local` | `org` | `bank`, default `org`): + +- **Local pool** (always): `project_scope:` for code_review and supply_chain; `repo:` for scope, summary and audit (summary and audit also OR in their scopes' `project_scope:` ids) +- **Remote pool** (`org`: `org:` AND NOT `repo:`; `bank`: NOT `repo:`): always raw facts, labelled with their source repo. Takes a fixed 25% share of `max_tokens` (and of `max_tool_tokens` for the tool); local facets split the rest. With `local` reach the local pool gets the whole budget. + +### `recall_memory` tool + +RLM agents (code_review, scope, supply_chain) get `async recall_memory(query: str, reach: str = "local") -> str`. A reach above the configured one is clamped; at most `max_tool_calls` calls per agent call (then `"recall limit reached"`); errors return `"memory unavailable"`. + +`reflects=0` disables the reflect loop, but the tool still works in raw-recall mode (`details.mode="recall"`). Only `max_tool_calls=0` (the **default**) turns it off. + +### Reflect loop tuning + +`memory.prefrontal.reflects` (env `MEMORY_PREFRONTAL_REFLECTS`, default `3`): + +- `0`: raw facts only — no LLM at read time, no briefings +- `N > 0`: both Prefrontal and briefing refreshes run at Hindsight's LOW budget (0.5× multiplier), with a doubled global cap (2 × N), so each gets exactly N iterations + +**Environment override**: if `HINDSIGHT_API_REFLECT_MAX_ITERATIONS` is set, it is used as-is (not doubled). -### Entity Relationship Diagram +**Hindsight LLM timeout/retry**: `llm.timeout` / `llm.retries` apply to all Hindsight calls. Operator overrides (precedence: env > codespy settings): + +- **Global**: `HINDSIGHT_API_LLM_TIMEOUT` / `HINDSIGHT_API_LLM_MAX_RETRIES` +- **Per-operation**: + - `HINDSIGHT_API_RETAIN_LLM_TIMEOUT` / `HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES` + - `HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT` / `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_RETRIES` + - `HINDSIGHT_API_REFLECT_LLM_TIMEOUT` / `HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES` + - `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_TIMEOUT` / `HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MAX_RETRIES` + +### Model + +`memory.prefrontal.recall.model` (env `MEMORY_RECALL_MODEL`) is the model Hindsight's reflect loop uses. Falls back to `memory.cerebral.retain.model`, then `llm.default_model`. Unused when `reflects = 0`. + +## Monitoring and cost + +### Logs + +Every recall is logged: + +``` +Prefrontal[review]: load status=ok 1 briefings, facets context=1 seen_before=3 ... remote=5 (512 tokens, reach=org, reflects=3) model=... calls=3 in=5120 out=410 cost=$0.0042 latency=6400ms maps=0 thoughts=256 tools=... +Prefrontal[code_review]: recall_memory reach=local status=ok mode=reflect model=... calls=2 in=2210 out=180 cost=$0.0017 latency=3100ms query='...' +``` + +### Episodic store + +One `recalls` row per recall, with PK `(bank_id, id UUID)`, `run_id`, `task`, and nullable `episode_id` (NULL for run-level loads). Index: `idx_recalls_run (bank_id, run_id)`. + +### Cost buckets + +| Bucket | Description | +|--------|-------------| +| `memory_distiller` | Distiller LLM calls | +| `memory_cartographer` | Cartographer LLM calls | +| `memory_retain` | Fact extraction (retain) | +| `memory_embeddings` | Embeddings during retain | +| `memory_recall` | Prefrontal reflect LLM calls | +| `memory_recall_embeddings` | Embeddings during recall | +| `memory_consolidation` | Consolidation LLM calls | +| `memory_mental_models` | Mental-model refresh LLM calls | +| `memory_other` | Uncategorized LLM calls | + +### Gating and failure + +Prefrontal is active for a signature only when its memory is enabled and Cerebral is available; otherwise the agent behaves exactly as before. Every failure in Prefrontal or mental-model upkeep logs a warning and degrades to less context, or to `""`. + +## Database schema + +Auto-created by `EpisodeStore.ensure_schema()` on first connect. ```mermaid erDiagram @@ -81,14 +281,14 @@ erDiagram banks { varchar(64) id PK - text description + text description "NULLABLE" } topics { varchar(64) bank_id PK, FK varchar(256) id PK varchar(64) type - text description + text description "NOT NULL" } episodes { @@ -112,11 +312,11 @@ erDiagram varchar(48) id PK int version PK "DEFAULT 1" varchar(32) type - text content + text content "NULLABLE" uuid episode_id FK int step "DEFAULT 0" - varchar(8) op_type "ADD | REPLACE | DELETE" - text previous_content + varchar(8) op_type "ADD | REPLACE | DELETE | EVICT" + text previous_content "NULLABLE" int ordinal "DEFAULT 0" } @@ -136,6 +336,29 @@ erDiagram text content } + recalls { + varchar(64) bank_id PK, FK + uuid id PK + varchar(48) run_id + varchar(64) task + uuid episode_id FK "NULLABLE" + timestamptz timestamp + varchar(8) kind "load | tool" + text query "DEFAULT ''" + varchar(8) reach "local | org | bank" + int reflects "DEFAULT 0" + varchar(8) status "ok | empty | limit | error" + text text "DEFAULT ''" + text model "DEFAULT ''" + int llm_calls "DEFAULT 0" + int input_tokens "DEFAULT 0" + int output_tokens "DEFAULT 0" + double input_cost "DEFAULT 0" + double output_cost "DEFAULT 0" + int latency_ms "DEFAULT 0" + jsonb details "DEFAULT '{}'" + } + banks ||--o{ topics : "has" banks ||--o{ episodes : "has" banks ||--o{ observations : "has" @@ -143,58 +366,31 @@ erDiagram topics ||--o{ episode_topics : "tags" episodes ||--o{ observations : "creates" episodes ||--o{ artifacts : "produces" + episodes ||--o{ recalls : "recalled" observations ||--o{ observation_topics : "scoped to" topics ||--o{ observation_topics : "scopes" ``` -### Indexes - -| 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)` | - -### Notes - +**Notes:** - All tables cascade-delete from `banks` -- `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE history per observation +- `observations` is versioned: PK `(bank_id, id, version)` tracks ADD/REPLACE/DELETE/EVICT history - `episode_topics` and `observation_topics` are M:N junction tables - `observation_topics.observation_occurrence` counts cumulative topic associations across all versions; `version_occurrence` counts within one version +- All columns except those marked "NULLABLE" are NOT NULL - Requires PostgreSQL extension: `pg_trgm` -## Reflection Pipeline - -After each agent run (at `end_episode()`): - -1. **Distiller** — Analyzes the agent's trajectory (head 60% + tail 40%, capped at `max_trajectory_tokens`) and proposes `CacheCandidate` observations for context memory -2. **Cartographer** — Takes candidates + current context memory, decides operations: - - `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 observation with an `ObservationTag`: - -- **`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. - -## Token Budgets - -| Budget | Env Var | Default | Purpose | -|--------|---------|---------|---------| -| Context memory | `MEMORY_MAX_CONTEXT_MEMORY_TOKENS` | 16384 | Ceiling on persisted ContextMemory (re-sent every iteration) | -| 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 | - -Observation capacity ≈ context_memory_tokens / observation_tokens (16384/512 = 32 observations) +### SQL example + +```sql +-- Run-level recalls (task='review', episode_id IS NULL) +SELECT r.task, r.run_id, r.kind, r.status, r.model, r.llm_calls, + r.input_tokens, r.output_tokens, r.input_cost + r.output_cost AS cost, + r.latency_ms, r.query, r.text +FROM recalls r +WHERE r.bank_id = 'codebase' AND r.task = 'review' AND r.episode_id IS NULL +ORDER BY r.timestamp DESC +LIMIT 50; +``` ## Configuration @@ -207,41 +403,70 @@ 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` | `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 | -| `MEMORY_BANK_ID` | `memory.bank_id` | `codespy` | Scopes all memory data | -| `MEMORY_DEFAULT_ENABLED` | `memory.default_enabled` | `false` | Enable memory globally | -| `MEMORY_COMPACT_TRAJECTORY` | `memory.compact_trajectory` | `true` | Apply head+tail trajectory bounding before distillation | +| `MEMORY_BANK_ID` | `memory.bank_id` | `codebase` | Scopes all memory data | +| `MEMORY_ENABLED` | `memory.enabled` | `false` | Enable memory globally (episodic + semantic) | + +### Hippocampus Settings + +| Env Var | YAML Path | Default | Description | +|---------|-----------|---------|-------------| +| `MEMORY_COMPACT_TRAJECTORY` | `memory.hippocampus.compact_trajectory` | `true` | Apply head+tail trajectory bounding before distillation | +| `MEMORY_MAX_HIPPOCAMPUS_TOKENS` | `memory.hippocampus.max_hippocampus_tokens` | `16384` | Ceiling on persisted ContextMemory | +| `MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS` | `memory.hippocampus.max_hippocampus_item_tokens` | `512` | Soft per-observation token limit | +| `MEMORY_MAX_TRAJECTORY_TOKENS` | `memory.hippocampus.max_trajectory_tokens` | `16384` | Cap on trajectory fed to Distiller | +| `MEMORY_MAX_QUESTION_TOKENS` | `memory.hippocampus.max_question_tokens` | `8192` | Cap on serialized reflection inputs | + +### Cerebral Settings + +| Env Var | YAML Path | Default | Description | +|---------|-----------|---------|-------------| +| `MEMORY_RETAIN_MODEL` | `memory.cerebral.retain.model` | Nemotron Super | Model for fact extraction | +| `MEMORY_RETAIN_CHUNK_SIZE` | `memory.cerebral.retain.chunk_size` | `12288` | Chars per extraction chunk (< 64000) | +| `MEMORY_CONSOLIDATION_MODEL` | `memory.cerebral.consolidation.model` | → retain | Model for consolidation | +| `MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE` | `memory.cerebral.consolidation.max_observations_per_scope` | `100` | Per-scope cap (-1=unlimited, 0=no new) | +| `MEMORY_MENTAL_MODELS_MODEL` | `memory.cerebral.mental_models.model` | Nemotron Super | Briefing refresh model | +| `MEMORY_MENTAL_MODELS_MAX_TOKENS` | `memory.cerebral.mental_models.max_tokens` | `2048` | Briefing size | +| `MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS` | `memory.cerebral.mental_models.min_refresh_seconds` | `0` | Min seconds between refreshes | +| `MEMORY_EMBEDDINGS_MODEL` | `memory.cerebral.embeddings.model` | Auto-derived | Embeddings model | +| `MEMORY_EMBEDDINGS_MAX_INPUT_CHARS` | `memory.cerebral.embeddings.max_input_chars` | `null` | Max chars per embedding (null=auto, 0=off, N=cap) | + +### Prefrontal Settings + +| Env Var | YAML Path | Default | Description | +|---------|-----------|---------|-------------| +| `MEMORY_PREFRONTAL_REFLECTS` | `memory.prefrontal.reflects` | `3` | Reflect iterations; 0 = raw facts, no LLM, no briefings | +| `MEMORY_RECALL_MODEL` | `memory.prefrontal.recall.model` | Nemotron Super | Reflect loop model; also mental-model fallback | +| `MEMORY_RECALL_REACH` | `memory.prefrontal.recall.reach` | `org` | `local`, `org` or `bank` | +| `MEMORY_RECALL_MAX_TOKENS` | `memory.prefrontal.recall.max_tokens` | `8192` | Pre-call context budget | +| `MEMORY_RECALL_MAX_TOOL_TOKENS` | `memory.prefrontal.recall.max_tool_tokens` | `2048` | Tool result budget | +| `MEMORY_RECALL_MAX_TOOL_CALLS` | `memory.prefrontal.recall.max_tool_calls` | `0` | Tool calls per agent call (0 = tool off) | ### Reflection Module LLM Overrides | Module | Env Var Pattern | YAML Path | |--------|----------------|-----------| -| Distiller | `MEMORY_DISTILLER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS,MAX_ITERS,MAX_LLM_CALLS}` | `memory.distiller.*` | -| Cartographer | `MEMORY_CARTOGRAPHER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS,MAX_ITERS,MAX_LLM_CALLS}` | `memory.cartographer.*` | +| Distiller | `MEMORY_DISTILLER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS,MAX_ITERS,MAX_LLM_CALLS}` | `memory.hippocampus.distiller.*` | +| Cartographer | `MEMORY_CARTOGRAPHER_{MODEL,REASONING_EFFORT,TEMPERATURE,MAX_TOKENS,MAX_ITERS,MAX_LLM_CALLS}` | `memory.hippocampus.cartographer.*` | -### Per-Signature Memory Overrides +### Model fallback chains -Each signature's `memory:` block in YAML (or `_MEMORY_*` env vars): +| Setting | Fallback chain | +|---------|----------------| +| `memory.cerebral.retain.model` | `DEFAULT_MODEL` | +| `memory.cerebral.consolidation.model` | `retain.model` → `DEFAULT_MODEL` | +| `memory.cerebral.mental_models.model` | `prefrontal.recall.model` → `retain.model` → `DEFAULT_MODEL` | +| `memory.prefrontal.recall.model` | `retain.model` → `DEFAULT_MODEL` | -| Setting | Env Var Suffix | Description | -|---------|---------------|-------------| -| enabled | `_MEMORY_ENABLED` | Enable/disable memory for this signature | - -Example: `CODE_REVIEW_MEMORY_ENABLED=true` - -See [Configuration](configuration.md#recommended-model-strategy) for recommended reflection models. +Every default is non-null (Nemotron), so a fallback only applies when a value is explicitly set to `null`. ## Quick Start -Enable memory for code review: +Enable memory: ```bash -MEMORY_DEFAULT_ENABLED=true -# Or per-signature: -CODE_REVIEW_MEMORY_ENABLED=true -SUMMARY_MEMORY_ENABLED=true +MEMORY_ENABLED=true ``` Recommended mid-tier reflection model: @@ -262,7 +487,7 @@ MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 ```yaml - name: Run CodeSpy Review - uses: khezen/codespy@v1 + uses: khezen/codespy@v2 with: model: 'anthropic/claude-opus-4-6' anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -273,6 +498,8 @@ MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 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' + # memory-retain-model: 'anthropic/claude-sonnet-4-5-20250929' + # memory-recall-model: 'anthropic/claude-sonnet-4-5-20250929' ``` > **Note:** pg0-embedded is included in the Docker image. For persistent memory @@ -280,4 +507,4 @@ MEMORY_CARTOGRAPHER_MODEL=anthropic/claude-sonnet-4-5-20250929 --- -[← Back to README](../README.md#documentation) + [← Back to README](../README.md#documentation) diff --git a/docs/usage.md b/docs/usage.md index f4d292b..1170fe8 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -162,7 +162,7 @@ jobs: steps: - name: Run CodeSpy Review - uses: khezen/codespy@v1 + uses: khezen/codespy@v2 with: model: 'anthropic/claude-opus-4-6' anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -187,7 +187,7 @@ jobs: steps: - name: Run CodeSpy Review - uses: khezen/codespy@v1 + uses: khezen/codespy@v2 with: model: 'anthropic/claude-opus-4-6' anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} @@ -257,10 +257,11 @@ codespy review https://github.com/owner/repo/pull/123 --no-stdout --git-comment **Enable via configuration:** ```bash # Environment variable -export OUTPUT_GIT=true +export REVIEW_OUTPUT_GIT=true # Or in codespy.yaml -output_git: true +review: + output_git: true ``` **Features:** diff --git a/poetry.lock b/poetry.lock index b5d7760..48f4fac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,30 @@ # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +[[package]] +name = "agent-detector" +version = "2.0.0" +description = "Detect AI coding agents from their execution environment or User-Agent" +optional = false +python-versions = ">=3.9" +files = [ + {file = "agent_detector-2.0.0-py3-none-any.whl", hash = "sha256:22c6a1c9c23894a12f55aea625f060b8d3330fba774de72d02d5442bb580417f"}, + {file = "agent_detector-2.0.0.tar.gz", hash = "sha256:19c8ae185dbdca23c24abdcd8ac2cf58b8e84dc284c6f6db536656601f9ffc3f"}, +] + +[[package]] +name = "aiofile" +version = "3.12.3" +description = "Asynchronous file operations." +optional = false +python-versions = ">=3.11" +files = [ + {file = "aiofile-3.12.3-py3-none-any.whl", hash = "sha256:5c1bcc9e929c50834608e8cc1a4cc1d7503eb60c15a535b779fd39e2f372c017"}, + {file = "aiofile-3.12.3.tar.gz", hash = "sha256:caa6aa746b5e47e2165f7abd741b6415e49cf4d44fddc0f61844612cc3924d41"}, +] + +[package.dependencies] +caio = ">=0.12.0,<0.13.0" + [[package]] name = "aiohappyeyeballs" version = "2.7.1" @@ -167,6 +192,25 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} +[[package]] +name = "alembic" +version = "1.20.0" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.10" +files = [ + {file = "alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d"}, + {file = "alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=2.0" +typing-extensions = ">=4.12" + +[package.extras] +tz = ["tzdata"] + [[package]] name = "annotated-doc" version = "0.0.5" @@ -189,90 +233,203 @@ files = [ {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] +[[package]] +name = "anthropic" +version = "1.8.0" +description = "The official Python library for the anthropic API" +optional = false +python-versions = ">=3.10" +files = [ + {file = "anthropic-1.8.0-py3-none-any.whl", hash = "sha256:79a4516a21e64fd7b15be1a49ebf544bd6376c96a971a77365358823a2717bc6"}, + {file = "anthropic-1.8.0.tar.gz", hash = "sha256:9c1783ed90f409617749a61c5ab98e20624a572626f2e0a15cea03ed8e1401e5"}, +] + +[package.dependencies] +anyio = ">=4.1.0,<5" +docstring-parser = ">=0.15,<1" +httpx2 = ">=2.0.0,<3" +jiter = ">=0.4.0,<1" +pydantic = ">=1.10.0,<3" +sniffio = ">=1,<2" +typing-extensions = ">=4.14,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.10.0,<4)"] +aws = ["boto3 (>=1.28.57,<2)", "botocore (>=1.31.57,<2)"] +bedrock = ["boto3 (>=1.28.57,<2)", "botocore (>=1.31.57,<2)"] +google-cloud = ["google-auth[requests] (>=2,<3)"] +mcp = ["mcp (>=1.0,<3)"] +vertex = ["google-auth[requests] (>=2,<3)"] +webhooks = ["standardwebhooks (>=1.0.1,<2)"] + [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.1" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" files = [ - {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, - {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, + {file = "anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101"}, + {file = "anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94"}, ] [package.dependencies] idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} [package.extras] trio = ["trio (>=0.32.0)"] +[[package]] +name = "asgiref" +version = "3.12.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.10" +files = [ + {file = "asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094"}, + {file = "asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340"}, +] + +[package.extras] +mypy = ["mypy (>=1.14.0)"] +tests = ["pytest", "pytest-asyncio"] + [[package]] name = "ast-serialize" -version = "0.8.0" +version = "0.11.2" description = "Python bindings for mypy AST serialization" optional = false python-versions = ">=3.7" files = [ - {file = "ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6"}, - {file = "ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16"}, - {file = "ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc"}, - {file = "ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5"}, - {file = "ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87"}, - {file = "ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6"}, - {file = "ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe"}, - {file = "ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a"}, - {file = "ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed"}, - {file = "ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51"}, - {file = "ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0"}, - {file = "ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010"}, + {file = "ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b"}, + {file = "ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f"}, + {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d"}, + {file = "ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365"}, + {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07"}, + {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484"}, + {file = "ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27"}, + {file = "ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450"}, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.9.0" +files = [ + {file = "asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61"}, + {file = "asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8"}, + {file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3"}, + {file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8"}, + {file = "asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095"}, + {file = "asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d"}, + {file = "asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c"}, + {file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da"}, + {file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9"}, + {file = "asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24"}, + {file = "asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad"}, + {file = "asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a"}, + {file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec"}, + {file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20"}, + {file = "asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8"}, + {file = "asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b"}, + {file = "asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403"}, + {file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2"}, + {file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602"}, + {file = "asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696"}, + {file = "asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44"}, + {file = "asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2"}, + {file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218"}, + {file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d"}, + {file = "asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b"}, + {file = "asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2"}, + {file = "asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7"}, + {file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c"}, + {file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d"}, + {file = "asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95"}, + {file = "asyncpg-0.31.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008"}, + {file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6"}, + {file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9"}, + {file = "asyncpg-0.31.0-cp39-cp39-win32.whl", hash = "sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6"}, + {file = "asyncpg-0.31.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a"}, + {file = "asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735"}, ] +[package.extras] +gssauth = ["gssapi", "sspilib"] + [[package]] name = "attrs" version = "26.1.0" @@ -284,6 +441,55 @@ files = [ {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] +[[package]] +name = "authlib" +version = "1.8.0" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.10" +files = [ + {file = "authlib-1.8.0-py2.py3-none-any.whl", hash = "sha256:88aebbd9af6757e14e912d5dc007ae1dc1f3e27e3b2152ce7c552ee2c3b3c121"}, + {file = "authlib-1.8.0.tar.gz", hash = "sha256:f3ecd5f1da737262fb53bf1a4d95c4ea1ad9dd509316587a255c99ab1838a4f0"}, +] + +[package.dependencies] +cryptography = ">=45.0.1" +joserfc = ">=1.6.1" + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + +[[package]] +name = "beartype" +version = "0.22.9" +description = "Unbearably fast near-real-time pure-Python runtime-static type-checker." +optional = false +python-versions = ">=3.10" +files = [ + {file = "beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2"}, + {file = "beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f"}, +] + +[package.extras] +dev = ["autoapi (>=0.9.0)", "celery", "click", "coverage (>=5.5)", "docutils (>=0.22.0)", "equinox", "fastmcp", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera (>=0.26.0)", "poetry", "polars", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyinstaller", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "setuptools", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "sqlalchemy", "torch", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray"] +doc-ghp = ["mkdocs-material[imaging] (>=9.6.0)", "mkdocstrings-python (>=1.16.0)", "mkdocstrings-python-xref (>=1.16.0)"] +doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "setuptools", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] +test = ["celery", "click", "coverage (>=5.5)", "docutils (>=0.22.0)", "equinox", "fastmcp", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera (>=0.26.0)", "poetry", "polars", "pygments", "pyinstaller", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray"] +test-tox = ["celery", "click", "docutils (>=0.22.0)", "equinox", "fastmcp", "jax[cpu]", "jaxtyping", "langchain", "mypy (>=0.800)", "nuitka (>=1.2.6)", "numba", "numpy", "pandera (>=0.26.0)", "poetry", "polars", "pygments", "pyinstaller", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch", "typer", "typing-extensions (>=3.10.0.0)", "xarray"] +test-tox-coverage = ["coverage (>=5.5)"] + [[package]] name = "beautifulsoup4" version = "4.15.0" @@ -308,17 +514,17 @@ lxml = ["lxml"] [[package]] name = "boto3" -version = "1.43.86" -description = "The AWS SDK for Python" +version = "1.43.103" +description = "The AWS SDK for Python (Boto3)" optional = false python-versions = ">=3.10" files = [ - {file = "boto3-1.43.86-py3-none-any.whl", hash = "sha256:94543a5ce482df8bb6a0bf8a944bf5ccf6625889b1df0d7886dfc3311ebd4169"}, - {file = "boto3-1.43.86.tar.gz", hash = "sha256:aca7b5d7f31a90ad37bec773552ec9bfb57e0029c9aa0f0f4d51d5bf9607b1c4"}, + {file = "boto3-1.43.103-py3-none-any.whl", hash = "sha256:574796187eb4e4fcaaf5c1b88c1724a1ba5a95bc27aeae5573676178666acc71"}, + {file = "boto3-1.43.103.tar.gz", hash = "sha256:524821052527f6446d249bf710847b032d9b12135e346c9751777a0a2811cf04"}, ] [package.dependencies] -botocore = ">=1.43.86,<1.44.0" +botocore = ">=1.43.103,<1.44.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.19.0,<0.20.0" @@ -327,13 +533,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.43.86" +version = "1.43.103" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.10" files = [ - {file = "botocore-1.43.86-py3-none-any.whl", hash = "sha256:4efc7fbd6e7616edbd55623bb585a044906149b9a5d3d8558420506526e5a5ac"}, - {file = "botocore-1.43.86.tar.gz", hash = "sha256:0e943c77ab6a54aaaf4d57e6026ede5c3dca341af71ce91f71849a6eae9d5dbf"}, + {file = "botocore-1.43.103-py3-none-any.whl", hash = "sha256:6a6a561a6de43845ae42bc7ff7e2c798742bfa277f5215398f9c15ed9b1bd67c"}, + {file = "botocore-1.43.103.tar.gz", hash = "sha256:8c7f220e09f3b7ec99c59c716aa66cf6f5b2b57fd8731544054f3778aa744c6a"}, ] [package.dependencies] @@ -346,15 +552,70 @@ crt = ["awscrt (==0.36.0)"] [[package]] name = "cachetools" -version = "7.1.8" +version = "7.2.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.10" files = [ - {file = "cachetools-7.1.8-py3-none-any.whl", hash = "sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b"}, - {file = "cachetools-7.1.8.tar.gz", hash = "sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43"}, + {file = "cachetools-7.2.0-py3-none-any.whl", hash = "sha256:3045213f186b89fdd95d94441354c4bd87c570b7a38d8c3fd1d9dd37f6dc90d8"}, + {file = "cachetools-7.2.0.tar.gz", hash = "sha256:bcac1a1b8da6909994a2957238a57b8140dab7c5c5c69a43669654fe87a33c1d"}, ] +[[package]] +name = "caio" +version = "0.12.9" +description = "Asynchronous file IO for Linux MacOS or Windows." +optional = false +python-versions = ">=3.10" +files = [ + {file = "caio-0.12.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:98e88c20217456113ae3021882aa310fab8ae77e54ec62a7c990f1c34a9f62a6"}, + {file = "caio-0.12.9-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:0b9922e7d5800b9aadafbe8ba956ed095ac37f85e0b38b96c97586e443548ff5"}, + {file = "caio-0.12.9-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c0432568cdf7cd36868f5d9800dfbb840d0ffaf0e8f2df0b42fbcbf83f2353ab"}, + {file = "caio-0.12.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17ec3d3cb28878e4771445f73161677a964f833f091fc88634a7214013dd8ed5"}, + {file = "caio-0.12.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3bf16c16b9318498ee7669a495d2198c44abdc0bc64483a500682c4eb41d5b52"}, + {file = "caio-0.12.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4edc95d8dc49f91fc9e870174860729fd088f9529464a7a6a1deda8a14d59a4f"}, + {file = "caio-0.12.9-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:14483697a27aefd265decb4b595e70814d284f46bc352c75eb9e9dae94a7b398"}, + {file = "caio-0.12.9-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:e10e3be34fca464cc5e27d3011e284fd0cde0cc488cda4742e765c8e9291647d"}, + {file = "caio-0.12.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:25255807af48386c50a2b186eaa582ffd5347d67c4f1cb3aa1bb79b36ace7af9"}, + {file = "caio-0.12.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:477d66e16948845d0f5ec6535e84c0c9ab7420b2f78f81bcf7b0f81d18a34ffb"}, + {file = "caio-0.12.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:554c6ee883a0f1e95236d85b8e7d8187b212668855ac3a858acb80f5e2e82e82"}, + {file = "caio-0.12.9-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:6e72fb0ddd369f712a4ad229ee0f1d7df6852e9b59b36fe8a46210b4c9ea8e82"}, + {file = "caio-0.12.9-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:f0698976f84dd40024204f0f77cf59ed7446e989575c8ad8cb64cd4b3e2871f3"}, + {file = "caio-0.12.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a06afa038f76324f439595b7a3242abf090a80b03006ed3d7dbbb9c031baf8a3"}, + {file = "caio-0.12.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed343077d8178c003b1596f40b1f46955be963a2871a1f85519be823113b24b0"}, + {file = "caio-0.12.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ed23f6ad6897fae4c02e3e8d236aa3cc18e766a7c5bb0352ea5e9fb26c5d3335"}, + {file = "caio-0.12.9-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:c327977b8174337c1aaff27da17ac249d176ef8ea6e2dbf70b49cdd8038d57d3"}, + {file = "caio-0.12.9-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:f6ffb3d448016d20d8c40c53864d81bdf7463157969de65841eeb370519dbf78"}, + {file = "caio-0.12.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a11421fb4ac591e6fea5512d9a8ad1b488e7b28cf610ede973bbfb4be5177454"}, + {file = "caio-0.12.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c3f6dc05486ce4e1027f1d2da4d84c1d6bb815886e76f34a9228d370d86a5537"}, + {file = "caio-0.12.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5c43eaebf220aeb9a388d309a5352e82562a7b9b2ea0102502204413641cfed7"}, + {file = "caio-0.12.9-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:bc63db6b4a54b2f1c519424acfb2b7198664e4ef8427d397215a81c0bed9e9e6"}, + {file = "caio-0.12.9-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:83718f0ba9ff56de9c3ce7a61b463466fbb064be4f087230064abac5d08b8100"}, + {file = "caio-0.12.9-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:4a69de19ef8780ea67f5fffa6fed95af32ed4c036e338a361307314306ac816c"}, + {file = "caio-0.12.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d3d3664c757d59d330381666683cdfa287c5bfff90819868e98ef0bb8c2d2382"}, + {file = "caio-0.12.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e152e1a1970d49b6056c1c94547801fcc6b41fc12185db6628ed58ec7785b04"}, + {file = "caio-0.12.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:32755af4459fed70d10a5e8c396de3ccb57e02365c7d818937a9794884b172da"}, + {file = "caio-0.12.9-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:de4458707370b9f13de2ead07e6719305624b4f0aad665ce6cb2f1452eb4965d"}, + {file = "caio-0.12.9-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:0a571981c8724f69c34ed4c7619585d552d27fe38bad0daf970dca3e14d6922b"}, + {file = "caio-0.12.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1c043b15a19e33c0b18b1937493be6730c3b08acc33af6899f669ac8957a3e46"}, + {file = "caio-0.12.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dada2e1ca5481e5c11201d269ac8e009ce86d34478f4426e9739470b0c2a1030"}, + {file = "caio-0.12.9-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:53d3febc3e46707786a023b28bd21213ac78332db46fae7df2a03c0cc208976d"}, + {file = "caio-0.12.9-cp315-cp315-manylinux_2_34_aarch64.whl", hash = "sha256:6634c57de5883819e0fb423094fe5cb480e81b8f5f46f601eb143e2c762f4c14"}, + {file = "caio-0.12.9-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:aa0fe6b459ef45d9d1fe82e22d5d849dcdb07e3a87f56444d1ff799249a08576"}, + {file = "caio-0.12.9-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:7fee93281b220488a5e6949ac197a52c2f1e877c6e32aec5ce658e9add3eba60"}, + {file = "caio-0.12.9-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6c28c4789a9c6d8e8b36cac15f85727bd8e4d312c98fc5aa4fceaa4631669786"}, + {file = "caio-0.12.9-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e600822c774fdb434e43163c7fc0d4dcfaa6540ebbf77e82e5f0dac071b27f8f"}, + {file = "caio-0.12.9-cp315-cp315t-manylinux_2_34_aarch64.whl", hash = "sha256:cc30e0c458d2d6785e72bb5117086ffce61b5c058bee5637c5aea69042db5e86"}, + {file = "caio-0.12.9-cp315-cp315t-manylinux_2_34_x86_64.whl", hash = "sha256:7490517a72f4ad01b39311ff8e909c3cab77f12dddd370ba6deb035313038468"}, + {file = "caio-0.12.9-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b30f3f36e45afb0814fc828cbb10ef9dd91043ff0b0a14ffe1cd8a348b461179"}, + {file = "caio-0.12.9-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b17b9aca6360f72bf07f1062bd60d8849f3fd5c1cb77f43923d8dec7fdbbb5df"}, + {file = "caio-0.12.9-py3-none-any.whl", hash = "sha256:bf12d4f014b2a33e642ed7905b5787656ede2fc24be21f7c086826ff0d32cec3"}, + {file = "caio-0.12.9.tar.gz", hash = "sha256:99e99419b44ab5511f7468c6a452887dd125b8e4042672a7589f0cf01d254ea8"}, +] + +[package.extras] +develop = ["aiomisc-pytest", "coveralls", "pytest", "pytest-cov", "pytest-rerunfailures", "setuptools"] + [[package]] name = "certifi" version = "2026.7.22" @@ -659,6 +920,31 @@ files = [ {file = "charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3"}, ] +[[package]] +name = "claude-agent-sdk" +version = "0.2.160" +description = "Python SDK for Claude Code" +optional = false +python-versions = ">=3.10" +files = [ + {file = "claude_agent_sdk-0.2.160-py3-none-macosx_11_0_arm64.whl", hash = "sha256:547cb16c94c67b57873c1ac9b5ce83076941d5189d52601b9791032d13a21456"}, + {file = "claude_agent_sdk-0.2.160-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:cc77386858bd5e716914544a35a0620fe1586d130299d1c7b2e207bebd02ace4"}, + {file = "claude_agent_sdk-0.2.160-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:59638c6acd0159da200eb9d8fbb282d6c8d2191e6fcdd02fa751857c014b7e40"}, + {file = "claude_agent_sdk-0.2.160-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:70d1dd4286b69ecdc334d66e7b9dae8532790803f7cd7e0bf269bd5e407f2259"}, + {file = "claude_agent_sdk-0.2.160.tar.gz", hash = "sha256:1107f7804c656e194cbf80d9e20b4b861803bc16c4067a04839a347c9d140f48"}, +] + +[package.dependencies] +anyio = ">=4.0.0" +jsonschema = ">=4.20.0" +mcp = ">=1.23.0,<3.0.0" +sniffio = ">=1.0.0" + +[package.extras] +dev = ["anyio[trio] (>=4.0.0)", "mypy (>=1.0.0)", "pytest (>=7.0.0)", "pytest-cov (>=4.0.0)", "ruff (>=0.1.0)"] +examples = ["asyncpg (>=0.27.0)", "boto3 (>=1.28.0)", "fakeredis (>=2.20.0)", "moto[s3] (>=5.0.0)", "redis (>=4.2.0)"] +otel = ["opentelemetry-api (>=1.20.0)"] + [[package]] name = "click" version = "8.5.0" @@ -681,6 +967,42 @@ files = [ {file = "cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414"}, ] +[[package]] +name = "cobble" +version = "0.1.4" +description = "Create data objects" +optional = false +python-versions = ">=3.5" +files = [ + {file = "cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44"}, + {file = "cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa"}, +] + +[[package]] +name = "cohere" +version = "7.1.1" +description = "" +optional = false +python-versions = "<4.0,>=3.10" +files = [ + {file = "cohere-7.1.1-py3-none-any.whl", hash = "sha256:5b5dd9034f8eb5a32c988edad7d0aea120bc3076dffbec9e84858d7b0f0361a0"}, + {file = "cohere-7.1.1.tar.gz", hash = "sha256:3b0fe2e4ab255db4939783c085d935d1f694f09fd625d60ce14c94fd92792889"}, +] + +[package.dependencies] +fastavro = ">=1.9.4,<2.0.0" +httpx = ">=0.25.0" +pydantic = ">=1.9.2" +pydantic-core = ">=2.18.2,<3.0.0" +requests = ">=2.0.0,<3.0.0" +tokenizers = ">=0.15,<1" +types-requests = ">=2.0.0,<3.0.0" +typing_extensions = ">=4.0.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.14.1,<4)", "httpx-aiohttp (>=0.1.8,<0.2.0)"] +oci = ["oci (>=2.165.0,<3.0.0)"] + [[package]] name = "colorama" version = "0.4.6" @@ -692,6 +1014,37 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "croniter" +version = "6.2.4" +description = "croniter provides iteration for datetime object with cron like format" +optional = false +python-versions = ">=3.9" +files = [ + {file = "croniter-6.2.4-py3-none-any.whl", hash = "sha256:8ef3d544107a5c05a150a2d78f8bf5a8eb9c5c4d93405a736b824109574e3f4d"}, + {file = "croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189"}, +] + +[package.dependencies] +python-dateutil = "*" + [[package]] name = "cryptography" version = "50.0.1" @@ -753,6 +1106,52 @@ cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy [package.extras] ssh = ["bcrypt (>=3.1.5)"] +[[package]] +name = "cyclopts" +version = "5.0.0" +description = "Intuitive, easy CLIs based on type hints." +optional = false +python-versions = ">=3.11" +files = [ + {file = "cyclopts-5.0.0-py3-none-any.whl", hash = "sha256:69c5d98bd48827a0f7c663b76f966f730652cbc48daa7625f1e69ae429b82376"}, + {file = "cyclopts-5.0.0.tar.gz", hash = "sha256:8bee8822319f5cd90711f568aec54a0a02bd3571ddff45c94aa62405beed38b6"}, +] + +[package.dependencies] +attrs = ">=23.1.0" +docstring-parser = ">=0.15,<4.0" +rich = ">=13.6.0" +rich-rst = ">=2.0.1,<3" + +[package.extras] +debug = ["ipdb (>=0.13.9)", "line-profiler (>=3.5.1)"] +dev = ["coverage[toml] (>=5.1)", "mkdocs (>=1.4.0)", "pexpect (>=4.9.0)", "pre-commit (>=2.16.0)", "pydantic (>=2.11.2,<3.0.0)", "pytest (>=8.2.0)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)", "pytest-timeout (>=2.3.0)", "pyyaml (>=6.0.1)", "syrupy (>=4.0.0)", "toml (>=0.10.2,<1.0.0)", "trio (>=0.10.0)"] +docs = ["gitpython (>=3.1.31)", "myst-parser[linkify] (>=3.0.1,<6.0.0)", "sphinx (>=7.4.7,<8.2.0)", "sphinx-autodoc-typehints (>=1.25.2,<4.0.0)", "sphinx-copybutton (>=0.5,<1.0)", "sphinx-rtd-dark-mode (>=1.3.0,<2.0.0)", "sphinx-rtd-theme (>=3.0.0,<4.0.0)"] +mkdocs = ["markdown (>=3.3)", "mkdocs (>=1.4.0)", "pymdown-extensions (>=10.0)"] +trio = ["trio (>=0.10.0)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "dateparser" +version = "1.4.3" +description = "Date parsing library designed to parse dates from HTML pages" +optional = false +python-versions = ">=3.10" +files = [ + {file = "dateparser-1.4.3-py3-none-any.whl", hash = "sha256:cbce86c64e0cea5c54c84c015d34c903d46c51bfad50632bf2f05a6141d10c05"}, + {file = "dateparser-1.4.3.tar.gz", hash = "sha256:bab8c43a746266e68142f4926e69438ce551441aa88e54e78bb6410bf3ee7000"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +pytz = ">=2024.2" +regex = ">=2024.9.11" +tzlocal = ">=0.2" + +[package.extras] +calendars = ["convertdate (>=2.2.1)", "hijridate"] +langdetect = ["langdetect (>=1.0.0)"] + [[package]] name = "ddgs" version = "9.16.0" @@ -774,6 +1173,28 @@ 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-PyYAML", "types-Pygments", "types-pexpect", "types-ujson"] mcp = ["mcp (>=2.0)"] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "detect-installer" +version = "0.2.1" +description = "Detect how a Python package was installed and get the correct upgrade command" +optional = false +python-versions = ">=3.10" +files = [ + {file = "detect_installer-0.2.1-py3-none-any.whl", hash = "sha256:87e2ce7d05cb108b178f8e45bb80d3d88c0cb4b397983345c18ad6562e02e620"}, + {file = "detect_installer-0.2.1.tar.gz", hash = "sha256:85f889d4d19c1caf5bef89ef389eb920cd8c6c2a868e1eeceec24527b93021e7"}, +] + [[package]] name = "diskcache" version = "5.6.3" @@ -796,15 +1217,51 @@ files = [ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +optional = false +python-versions = ">=3.8" +files = [ + {file = "docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b"}, + {file = "docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015"}, +] + +[package.extras] +dev = ["pre-commit (>=2.16.0)", "pydoctor (>=25.4.0)", "pytest"] +docs = ["pydoctor (>=25.4.0)"] +test = ["pytest"] + [[package]] name = "dspy" -version = "3.3.1" +version = "3.4.0" description = "DSPy" optional = false python-versions = "<3.15,>=3.10" files = [ - {file = "dspy-3.3.1-py3-none-any.whl", hash = "sha256:250049f565f52c014609ce2d3ca0de17a6c9449ac961492d61a009aa30dceabf"}, - {file = "dspy-3.3.1.tar.gz", hash = "sha256:ca53a428ac6a30984a894cf3847cf623b7dd7e8aadf8a63620b1f1f6155e4eaf"}, + {file = "dspy-3.4.0-py3-none-any.whl", hash = "sha256:36de329ecdaf18e31977e26c61b587caa470a34f574f2fe6cb7f4c82bcbf97cc"}, + {file = "dspy-3.4.0.tar.gz", hash = "sha256:4dea089c6bd1f8d314e1095e7f64b4b4ff64bb8a09549efee026c0a609f08c16"}, ] [package.dependencies] @@ -818,7 +1275,7 @@ litellm = ">=1.65.8" mcp = {version = "*", optional = true, markers = "python_version >= \"3.10\" and extra == \"mcp\""} openai = ">=1.66.2" orjson = ">=3.9.0" -pydantic = ">=2.0" +pydantic = ">=2.11.0" regex = ">=2023.10.3" requests = ">=2.31.0" tenacity = ">=8.2.3" @@ -834,86 +1291,536 @@ mcp = ["mcp"] numpy = ["numpy (>=1.26.0)"] optuna = ["optuna (>=3.4.0)"] test-extras = ["datasets (>=2.14.6)", "langchain_core (>=0.3.0)", "mcp", "numpy (>=1.26.0)", "optuna (>=3.4.0)", "pandas (>=2.1.1)"] +typesafe = ["typesafe-sdk (>=0.6.0,<1.0.0)"] weaviate = ["weaviate-client (>=4.5.4,<4.22.0)"] [[package]] -name = "fastuuid" -version = "0.14.0" -description = "Python bindings to Rust's UUID library." +name = "email-validator" +version = "2.3.0" +description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a"}, - {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00"}, - {file = "fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470"}, - {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d"}, - {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8"}, - {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219"}, - {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6"}, - {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe"}, - {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d"}, - {file = "fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a"}, - {file = "fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4"}, - {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34"}, - {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7"}, - {file = "fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1"}, - {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc"}, - {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8"}, - {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7"}, - {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73"}, - {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36"}, - {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94"}, - {file = "fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24"}, - {file = "fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa"}, - {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a"}, - {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d"}, - {file = "fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070"}, - {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796"}, - {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09"}, - {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8"}, - {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741"}, - {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057"}, - {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8"}, - {file = "fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176"}, - {file = "fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397"}, - {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021"}, - {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc"}, - {file = "fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5"}, - {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f"}, - {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87"}, - {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b"}, - {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022"}, - {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995"}, - {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab"}, - {file = "fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad"}, - {file = "fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed"}, - {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad"}, - {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b"}, - {file = "fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714"}, - {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f"}, - {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f"}, - {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75"}, - {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4"}, - {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad"}, - {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8"}, - {file = "fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06"}, - {file = "fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a"}, - {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:47c821f2dfe95909ead0085d4cb18d5149bca704a2b03e03fb3f81a5202d8cea"}, - {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3964bab460c528692c70ab6b2e469dd7a7b152fbe8c18616c58d34c93a6cf8d4"}, - {file = "fastuuid-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c501561e025b7aea3508719c5801c360c711d5218fc4ad5d77bf1c37c1a75779"}, - {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dce5d0756f046fa792a40763f36accd7e466525c5710d2195a038f93ff96346"}, - {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193ca10ff553cf3cc461572da83b5780fc0e3eea28659c16f89ae5202f3958d4"}, - {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0737606764b29785566f968bd8005eace73d3666bd0862f33a760796e26d1ede"}, - {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0976c0dff7e222513d206e06341503f07423aceb1db0b83ff6851c008ceee06"}, - {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6fbc49a86173e7f074b1a9ec8cf12ca0d54d8070a85a06ebf0e76c309b84f0d0"}, - {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:de01280eabcd82f7542828ecd67ebf1551d37203ecdfd7ab1f2e534edb78d505"}, - {file = "fastuuid-0.14.0-cp38-cp38-win32.whl", hash = "sha256:af5967c666b7d6a377098849b07f83462c4fedbafcf8eb8bc8ff05dcbe8aa209"}, - {file = "fastuuid-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3091e63acf42f56a6f74dc65cfdb6f99bfc79b5913c8a9ac498eb7ca09770a8"}, - {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2ec3d94e13712a133137b2805073b65ecef4a47217d5bac15d8ac62376cefdb4"}, - {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:139d7ff12bb400b4a0c76be64c28cbe2e2edf60b09826cbfd85f33ed3d0bbe8b"}, - {file = "fastuuid-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d55b7e96531216fc4f071909e33e35e5bfa47962ae67d9e84b00a04d6e8b7173"}, - {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0eb25f0fd935e376ac4334927a59e7c823b36062080e2e13acbaf2af15db836"}, - {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089c18018fdbdda88a6dafd7d139f8703a1e7c799618e33ea25eb52503d28a11"}, + {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, + {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.141.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.10" +files = [ + {file = "fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3"}, + {file = "fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1"}, +] + +[package.dependencies] +annotated-doc = ">=0.0.2" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""} +fastapi-cli = {version = ">=0.0.32", extras = ["standard"], optional = true, markers = "extra == \"standard\""} +fastar = {version = ">=0.9.0", optional = true, markers = "extra == \"standard\""} +httpx = {version = ">=0.23.0,<1.0.0", optional = true, markers = "extra == \"standard\""} +jinja2 = {version = ">=3.1.5", optional = true, markers = "extra == \"standard\""} +pydantic = ">=2.9.0" +pydantic-extra-types = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""} +pydantic-settings = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""} +python-multipart = {version = ">=0.0.18", optional = true, markers = "extra == \"standard\""} +starlette = ">=0.46.0" +typing-extensions = ">=4.8.0" +typing-inspection = ">=0.4.2" +uvicorn = {version = ">=0.12.0", extras = ["standard"], optional = true, markers = "extra == \"standard\""} + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-cli" +version = "0.0.32" +description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" +optional = false +python-versions = ">=3.10" +files = [ + {file = "fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4"}, + {file = "fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424"}, +] + +[package.dependencies] +fastapi-cloud-cli = {version = ">=0.1.1", optional = true, markers = "extra == \"standard\""} +rich-toolkit = ">=0.14.8" +typer = ">=0.16.0" +uvicorn = {version = ">=0.15.0", extras = ["standard"]} + +[package.extras] +new = ["fastapi-new (>=0.0.2)"] +standard = ["fastapi-cloud-cli (>=0.1.1)", "uvicorn[standard] (>=0.15.0)"] +standard-no-fastapi-cloud-cli = ["uvicorn[standard] (>=0.15.0)"] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.26.0" +description = "Deploy and manage FastAPI Cloud apps from the command line 🚀" +optional = false +python-versions = ">=3.10" +files = [ + {file = "fastapi_cloud_cli-0.26.0-py3-none-any.whl", hash = "sha256:94029f92f3dc4289b376e1213f0d6cc7c30d4f32de7e47b5effc6dc754ab9ce9"}, + {file = "fastapi_cloud_cli-0.26.0.tar.gz", hash = "sha256:5fd64c26228c8ead803461e8b58ae64ffd586e960d97ab1fc848efac2d9539be"}, +] + +[package.dependencies] +agent-detector = ">=1.1.0" +detect-installer = ">=0.1.0" +fastar = ">=0.10.0" +httpx = ">=0.27.0" +pydantic = [ + {version = ">=2.7.4", extras = ["email"], markers = "python_version < \"3.13\""}, + {version = ">=2.8.0", extras = ["email"], markers = "python_version == \"3.13\""}, +] +rich-toolkit = ">=0.20.5" +rignore = ">=0.5.1" +sentry-sdk = ">=2.20.0" +typer = ">=0.26.1" +uvicorn = {version = ">=0.17.6", extras = ["standard"]} + +[package.extras] +standard = ["uvicorn[standard] (>=0.15.0)"] + +[[package]] +name = "fastar" +version = "0.12.0" +description = "High-level bindings for the Rust tar crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastar-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:310c99a33f83f83f49917cee153cdd49ac0a3e42f78ad685a84a700ede3aece5"}, + {file = "fastar-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a57a4b3ea38937f95c8ac76432992dc29f672b265e021a4c8c7191a767188297"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:00104297bee6bec0c20aae27e82e62b33db4329575e4cfd3e824745128b9933a"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12484b5f87529e9c0af4299a5db779c92c0c393b95867977e3ea02d22024dc96"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e54319e21ea309e8f2141ed88ace0cb34af72bbd5865eb6a819773a170230e27"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2eef75f01b874968ac340be98d44a583ded3b50f29483fd3ec5fd86cce41db7d"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69391964bbf1b170eeed382fd05d490ba8f6b00fb0b72627ba00cf7d037b9d75"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7a28985d86a3d8582d6ce69d356fc498035d717938b5029df057d599718888"}, + {file = "fastar-0.12.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3060521ae526301acadd84fbc3bdf50faa63e3f06f7faecdb286c7e4e4d8b22d"}, + {file = "fastar-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:868259b82a03d90eb7b618478a5f5721f293f477b9a1f2037546e9edec21792a"}, + {file = "fastar-0.12.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:09e6a5d4b111c5d40f7dc0813357260ee060b74b4e14bfdc66f5c3ae37f9832c"}, + {file = "fastar-0.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:682a8c91d3ee82179e44ffd7673f4a6a08e7465970b75d19c38df41c39e2dbc2"}, + {file = "fastar-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a30eced7e4c711a272336e8c6214ca23f30eaf57b06c85eb6f67cbf50e207b7"}, + {file = "fastar-0.12.0-cp310-cp310-win32.whl", hash = "sha256:f5e9cec09cf46ba60ae78bdaf22548b6128d4142c274632faf6c8d2a7a6abf0f"}, + {file = "fastar-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:46feb733e23f2a85f097f5bab710e49ad3e4d9ad63d6a6c5b4c6d470239c6217"}, + {file = "fastar-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:474fa88aab2aa2ea32682edfe46cd4d24a5f75acaa2021b40d1e247e33e21b7f"}, + {file = "fastar-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dc6149218fa5e34526714aefabc148b25fc7d27c2f25c8b942f1567875511a9"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0e16b0d029411989b9459adc65b9b553a7051909a96e4432e4a151490e42f0de"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f0432d1138e7f3b65a3afc324ac373706751d1b8554541fe33d5e62ab08ff11"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:930d355b98304984b70b30b8a678c782ecfa958a9c710134c71ea54db995a21f"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e33b9b67d9fbcda49559c653390698a3e4d89c4d9cb9974fbbdd8a8aa0bfe3b5"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc98215d7cdd32b70d2ab8123448071f33e5cc4a4af2ddef67af37beb420dae2"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:471a2f3c7295c296252463ae2eadd87f258dec33a7fec053a553c90930e03392"}, + {file = "fastar-0.12.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:6bc9c294f2aca5590b8188ab09e04099b727444cee7e56bd89d73c57adcdb2b8"}, + {file = "fastar-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1c2926264f000e88e214a23c1a308a4851325ee72569c27b4188d8d44262fa56"}, + {file = "fastar-0.12.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:adfc7981f3b3aaafcebc555437e66a37bafbe48d4e3e2129b782b52ed96b6c06"}, + {file = "fastar-0.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:75ea567751a7eebbc5cee86e3df5f0e699afe984e6b76eed91fc304485b0808a"}, + {file = "fastar-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c19e0d943e68ee9748610e510639e438c07d4dda13743cbb2d928689aad15599"}, + {file = "fastar-0.12.0-cp311-cp311-win32.whl", hash = "sha256:423d2b0ff5a43405c9776f1ef0779502c547bdb2d7fa7857b4f7434c95035556"}, + {file = "fastar-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2fcbdc9b49fb240965941a21fd45ac9f7f75bf87acc33106e1ee70ab912cfdd"}, + {file = "fastar-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:e8fe9f8e8b6b4df05f1a3561ff9a5348553b93fa6fabc7e31334d1ee720c00af"}, + {file = "fastar-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d7a37d672233031bd41b000852e2520d34a1dd362c2e3b9000b6400a16b3bba4"}, + {file = "fastar-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:787e3c8018848b56b7e47918f9a9ec0e13a68e39af6e4d386cf083b2973d254b"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8fa10e692ca23aafb88ab441bb280724328da7b8cebddf3296c751b4e06324ed"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76115050e8f076fb96f1aa7662dc502b5232d21b610aec08035957a93a830cb1"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0df33b22848b96beb8b229869abcc6592592d654f25b6cb1856c08717aaf1544"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27142e096eb5765b4dca66e4dbd29a9c61a0a4bc4c0ca649f6e661475da2bf8d"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eaf9306c98e22aa7650c6ec02522d77b61c539bc81583897ca351de125ade14"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb0b3882626832361dd0fcee1f4ef1c6e2a7aaa4165e51c86ae0bab95caccd6"}, + {file = "fastar-0.12.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:47739deed9e4ccf6514821d1c7a79c9188e4a5029d80b618078739d91e7ed9f0"}, + {file = "fastar-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ec2100cea7274ec4b1ec77cd5613318a2d9fc9f7a291a6acf642a26e3856a93"}, + {file = "fastar-0.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f95a59a11ee59701a6c0048c90601063894102343c4bb09462eed5df0fab0077"}, + {file = "fastar-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8383404eebbff9832652ddcd32f91bb4acd8e038b27597dc12aef33c4e72f7b7"}, + {file = "fastar-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60de8b81a04cda78e5281d72ff6d0c50e4925621cc5eb55d99c2e344d1b291f4"}, + {file = "fastar-0.12.0-cp312-cp312-win32.whl", hash = "sha256:f62411993976d5c5a0d1090f8c809f96e0671d824ee0d844e5b5256bf31fe7a9"}, + {file = "fastar-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ed84522ddffb4c41c247f3e6823c763faf45af2ba1bf887eaab7f907e74e0bbe"}, + {file = "fastar-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:c03a61a149eaa857a4af8bf6c0bc0895bf668b8d5685130337a1ca42e60f9828"}, + {file = "fastar-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:654165090cdcac7ff13d43ee4012c366f0f2061ddf46658bc0ad248c8aa3960a"}, + {file = "fastar-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:685d3d45943b43c32c71c8470552a615c90e06ca532db1b7a5633f01aa108f0d"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f8da75b5eca0d3b540e50ab6b7dfe4b148d7ae8a0b444a9781fd1219392859a0"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13c2df8db1b7a4d783429ffa1125c53f7dd9534baae7218eb49273797691e2a8"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:042fd43c4e0c3f3ba3f7b8a083694d1c4bd77d60ce266090d4eb96cb8a8021c2"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6958a332c55052dd8090b03de238ca59190d625f4ba9c292b34e938ac64105ea"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b05c0c739c43b9228e57fc59eb68c38660c62030bcdea3a032b269df71f5bb8"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2bfad69679111e4567d4bad41fd795071c9335cd94bc0f26e24b7d19e95c9b1"}, + {file = "fastar-0.12.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:1155e1dd9c60cf636b6b3d35edfe242348f47286724fa84b5b4055c03d7fdbf6"}, + {file = "fastar-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:29b87474b2e7c9e64549b87aeb2c1d68a94e78c887a3a8d88bf7b804ddbcc0dc"}, + {file = "fastar-0.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4587a08d6de2e62611278fc4cd36186a3ebbf6609d9e49df34d79a407d66f599"}, + {file = "fastar-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e7d7512b5c747edfce129448a72c8f6223323748ca3e98cc54401241bff70ee5"}, + {file = "fastar-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ce9a7e7757074a3d0920a8bc3936cf4164f63786d8e1b64e425d2957ddcceace"}, + {file = "fastar-0.12.0-cp313-cp313-win32.whl", hash = "sha256:61f1eec258b328182c6b6258641d33264ac3080fb7fcbf40ea1e326fc855d917"}, + {file = "fastar-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:84caa362865cac75807c51afbeff2e9b313fc45f89e0865f7c8bf627ea721f4b"}, + {file = "fastar-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:a3de985d942247fa924e185ff2744a6da0005dcedbb39ae1b811bec11710e572"}, + {file = "fastar-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:7116a770a4e47262734fafe06d3e835a23b833e81b45b3054558301385a0f2a7"}, + {file = "fastar-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebe324ffcb3e8efb2308255f60de911bfa4ccf10f087b6e60c62606f091f1807"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4af6be78f7ec6ef8e6da7d162361e4198be10fe81bfc95112f635c2c14e12922"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad38b27e93ba9c7de076ad694d224153bf8d66695acc27a114cba8087078fd54"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:968d64c203d10d257c2f2268cfc97d94f273fdfc64b37a4739fdb6e2cf2c3f03"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21256ccb3946730e3d601b6a9c9de61a127855957a7f0852cb14b0dbe15f8aeb"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d89116b102fb7d4c47b6b723dc32d5c12122e7ffeb41b7a8b22cc777c9eca5"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1450e0325897e87594c29837fdd9e338b1d281f6c81d14d7a75b6776285b5b6e"}, + {file = "fastar-0.12.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ca63fee43f07408efec09e1c0ae34a1b29ae52b8c1adc31bd6434ccc9e1741cb"}, + {file = "fastar-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fa5fd057b4f4537f04dd4e4f13be92433bf47bc479309335755531c5af34560"}, + {file = "fastar-0.12.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4b395c3d4375809d0d55b5ae297f6bc037b90b382f35f056e453732e4f6f523c"}, + {file = "fastar-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:20d5e52c45e75a55ed27e7952487506269e9a64559d6cbfaf6977529db81298f"}, + {file = "fastar-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4497098ebdf3c1f89dbeafe44ebcd7f143f8a774641bbfa13d1d793104464c41"}, + {file = "fastar-0.12.0-cp314-cp314-win32.whl", hash = "sha256:56cb3b3c46edf26f054f2420573c7c69c03997203a7a867ca835bc305b4a0f30"}, + {file = "fastar-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:81534df96e775ccaa37fcd1f45e06f48c245ff77e30d3bcb0d3c1101da9399e8"}, + {file = "fastar-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:fce60bd91fd982bf52e9a4c87820a44f92ac0d896bd64544891d6995fa6b8b98"}, + {file = "fastar-0.12.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:22c882f1096199d5e63f3ea4d9313e10799fcbc70166d315aa07576148601326"}, + {file = "fastar-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:74c928183d7ca19056bc0eb24d8c1907c115cfb2382ad4a7c32ccbfb04ca0a0b"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f1f3e70447f45e54b488eea8bd401cd3774b7b688d5b507915ae415058d8ac86"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96ae27bbb807e39e05097bdc2a6cba6820f31dd053aaf8cf0a045e5969041778"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0d60682ed24fc6063b18e76a947f1cc0fcd0777709357b801b3e3458a87c2e5"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:644f6d06e312bc47dd315aa36fba723854859601e184113de7b887613ed9a5ad"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6c5addb01f206bd75f784540a8a11bbddc451293318a823e3fecaf0d62cc3c"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc892486eb242ac55dde185d547d2723289da50c6cdc06614868416576ba5c4f"}, + {file = "fastar-0.12.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a47a68d1b9bd59062af41d9809a340739ab1ba13cb5b4beb23466a621d6479f2"}, + {file = "fastar-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a7d9fd762e7eab2262ec004aa9c714b53d303e11cca7c814b7b634d9d2424691"}, + {file = "fastar-0.12.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3766bf0aeeb6a03d114b185472593a2a3f0ff43f1b56c40fe0cda4283f9f4351"}, + {file = "fastar-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:860516a52a3bdcaa746be42fc1bdbbbd48e05c7579f9f61e30d78a7e065835bd"}, + {file = "fastar-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb75898c166ff6d232bceb68a5a7dc4a8f17239fb40925d169649a3967b76c4a"}, + {file = "fastar-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:c5755332572756061b29766a9ceff2c837d52d96828e58f908cb46cc49123bd7"}, + {file = "fastar-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:07d861c7ddf31bccb9615a0ba4c9f06987d1373a6b357183526d68cf9c3f5552"}, + {file = "fastar-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1d56e2a52bebd3e379d0cacc2b018b819a0b99f0dc19b4453f304c4e2fce5b3"}, + {file = "fastar-0.12.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:6109ec55528a975ab3644dc1c9ccccb2c2315daa66ca34f54e1e3dca60afa757"}, + {file = "fastar-0.12.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6e94e2881c3aa53da5d9161e2e64c0d698c66506a62024d5900943098220ebc7"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:682c531ac174d63919374eaab6fca91f92432cbf6a2262acb72719ba2e2a694d"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf3d5bfd73516b506a916f6809b90a4ad73ff5840c0af6cbe0142417a03b014"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbeb5fce858248d6b9b1fbef12c3e14d174432728c6e6eb1e2a63447432571c2"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45164f7138613b76918101ea28842cb69c10ff441c1ba2d56d7c6b28053f28e2"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e71715c64695bc80fd8fed2a82af30acb49b6bf085c06876cbfc2116b53cc7c"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8056fb0f3ff2213eb00234d32b2d701cd288b763d3430b67033a393b8d33b47"}, + {file = "fastar-0.12.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a843704912dc3b20e152743bd5fa3e225bf9cc23c34fea0debeceefead477e78"}, + {file = "fastar-0.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:b4a7b6ca4a04e269aa26533ca8bfd0c674e4ee7328b0d3d80d45ab979a7e613c"}, + {file = "fastar-0.12.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:883f1e06c0d9649a2e54b767b3384b47306098ea73be3ca288d562c4d73dbcaa"}, + {file = "fastar-0.12.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:74858c4648bdc4450a66f3d6ec16a4e61ba48c16c9898a88a061d3272f82c65b"}, + {file = "fastar-0.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b16451d5b50579e4eb7dc1761946bc6f6186df44fa84310a06f455c26eb4442e"}, + {file = "fastar-0.12.0-cp315-cp315-win32.whl", hash = "sha256:ad8185a7b379e5cd81ef65209d21db4c63e8a62bcaa2d17fe40a8e50fcb28427"}, + {file = "fastar-0.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:a2819b9061cee89da560156b77230d4ffe81e75b8f64b9732ca34d7bc546e49e"}, + {file = "fastar-0.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:a8a8130f236a5dc2ceab88486f77bbdd516d08dc949d0f04194305845cf44c19"}, + {file = "fastar-0.12.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:6f25c1aa6d55a457d95dc2163bbc27942e1541ca6792d4bf323a922688b8597e"}, + {file = "fastar-0.12.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:aeb69fe64537deec4902f45ad9634b85d44ebb42ee1a33725d6584e8d9b33927"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a83ae278bcc718dd155219fbbd552a16bd8c178effc5021600c3be2806a01cf"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048c732e3ca28a132732f83130ccdab58d9b27dd36bb26bdeb42c2d48827da9"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:613466f628667af03de8f914de58a07bfb3ee1bd0347e3532ec9790df92a1e72"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f59d3243d8913db385ab822be8f111f188218ea73f9f14f5d70c869a33ff4d1"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b362e5404dab262e85f0d93bd950933a0935dac6a9f5f0516bba6c703c440ec"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d1e50c423cd064f29f11c98f6d995b8fa7df7bbc19f3fdb9f081859afd8e00b"}, + {file = "fastar-0.12.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:6857a79691c5c033a31d76c62ad02f2c92d173a0e1fb2fac7fcb7ac686108bd3"}, + {file = "fastar-0.12.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3053bb800b5375fbca8f96d256654ae3489c439f1b55766896d1c703d8281804"}, + {file = "fastar-0.12.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:067094312cbea82ef2efa3999dc31318ac539805965c9505d99654f01775cac1"}, + {file = "fastar-0.12.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f69400ebb83a8d754aa7735165c12f8029ac577c3c08eb6d174eedc5901b7cb2"}, + {file = "fastar-0.12.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7d875d99946a11538f7ecb183f0a885d1d0a0495a6f1d6d2aa1de9b5fe6e5e9d"}, + {file = "fastar-0.12.0-cp315-cp315t-win32.whl", hash = "sha256:39dad3351f1399cd28e2e649b6651299ef857df6c745e598b00b4167dcf93dbc"}, + {file = "fastar-0.12.0-cp315-cp315t-win_amd64.whl", hash = "sha256:00cda9a3f11871261a4e77a3b8f0eede85c9730fb7516811bcbcf96a2bb3b75b"}, + {file = "fastar-0.12.0-cp315-cp315t-win_arm64.whl", hash = "sha256:e8e0fb057b5c271f46f3300b539b0d3dab8c8cb2515205a37c818c2f68d16806"}, + {file = "fastar-0.12.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c745781d4836c794800fdda4641f9d685b14e1981ed9d898c7ac152ac62cd1dd"}, + {file = "fastar-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:01d90bb64a83843a3ae07e33da300723de7ce50864d6bb47a58bc1112d68ecbf"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4468813c739aaf498afc6f553fe082b485d39c3ddd4fa4b1ca130ce6d82057fd"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4851974bf24c1d993617f5c045a323315b632ad48358dfc95f126d822937292a"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41d7d26a09ccc0e6a939194a640aa934ae5e263581fb0127c44b0b1dd720f071"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc8a2bfb1b52ea812eee3bf34f7c76ecc72b74e3346b46fc308212fa27c1f4a"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aee7a7354425a9841991b8032d94509c022b509cb0e47f484c7fb56df5874e3"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70fb44c26eb993bd676ccec24ce45b70354e4d1c0e35750a8a49233934049d7d"}, + {file = "fastar-0.12.0-cp38-cp38-manylinux_2_31_riscv64.whl", hash = "sha256:d9e403be00e17d340131327f97789c6b640ac516926e1217cfbe522c24b5120e"}, + {file = "fastar-0.12.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b3a2b7ec287bfe7110a1c047698d681a0a1c4b888c7f4aa93862de9b541928d9"}, + {file = "fastar-0.12.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6c068844bbfbc26b42c4142c53d994df6b6bb71146f7f860a7135d49d916e01a"}, + {file = "fastar-0.12.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:304152fb06608208581e3340f51e5f8d9658fd1ea66f204647ab931eb3525b24"}, + {file = "fastar-0.12.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b21937917620122291e65ed1644e8b8acb519fc7df44d2ea3b5b9963a5d804e9"}, + {file = "fastar-0.12.0-cp38-cp38-win32.whl", hash = "sha256:6aac6e5b0c9f140a3ee0bfd1037b3c178cf5b6f7f1ac37c002006bb34d8c90d8"}, + {file = "fastar-0.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:d03e32e47a98f02c6a99d894a07e54efea0b9c3eb6fa8aed4f81b075530b2c7f"}, + {file = "fastar-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7c9cad467ca85bb2629a5e4d52bc60997221fe91d19e93ca8c05735d23f72de9"}, + {file = "fastar-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2b8394d4c417dbc05c3a5c45e6c9742d5e78ce07e6eaf18894218c0a9dae5d0"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bfa2891ee4bd86b71721b0afa6c410c2917c86522dffc13a5aba2038d147e81f"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc34a941607a44563ec4b4f0f5c7af7f237bceab73c372f1987d505ecd6165c"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc7f09c635194e5e673703cffbd1eda80773edb8000faa42da158b8ea1ce5018"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d6929f821729abc10b754180675dcd2e5328ac12d28abef4409afebd1a8c59"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bec95651f31caa9f505bbeb2019187e2f0ec561548ae0cd33cfa753ef59492ac"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aef3e8f913fcf2150e664bfbdca9462c6472ad1c1c553f4b58c1b5d2b34807f5"}, + {file = "fastar-0.12.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:37e0d61c64eaf5eae1970c0e73a56c596b3487b14bd6cede3c6426d981cb2bf5"}, + {file = "fastar-0.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b79e69b2c9d0df293cda11881a1b27ab2893460a2b68229b20f7757fb5f5fe2d"}, + {file = "fastar-0.12.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:31196ca910579d0317fdbcefd7bbe82a08b5c933bd0a61269b66ca1feeb5c82d"}, + {file = "fastar-0.12.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b3f28f54bde75366cc1b9357ff739593fa61cd81736f0f31883ea026623108b2"}, + {file = "fastar-0.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9612513fbf7fa906bb186c3e30f51edb4b6baf27088a7d4b4ef0c6e8732b16b8"}, + {file = "fastar-0.12.0-cp39-cp39-win32.whl", hash = "sha256:1664bad53ed42a657d1c9b8a939d3a1b2716ec9feb00a65b12c5c9668e3128f2"}, + {file = "fastar-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:c46cf6767d71506b6917cddcc903b0b110169d5254717c95ea00e2920a85df3c"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fa463374dde07fbb7a46fddc75288554ce9b89f5d0129fa073028e2e13d6db3"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4da24effffd801bfa5d39c7b68c59a1124df8ce8ddb4048e1a5bbff4a915248b"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d3f780bd6d4e39df5b21862d23df636ad03fea45d73e105bba2493f467fe2b86"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172ab2b39a9307b0fc56e603b57d01d98f92bcd19cc78c1d3451142d00b3eee3"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d47478fc6ac75e6a2f7a0adf7d5d0636f36fbd2b1269bf87530c5bc404af1c1"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a84c2c04e77559363e7da7b3a99f94a48d938043de09bfc13042f4437ae1f86f"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b57afcac03ef59dc3b26b0669b8da7f61d0d8961896b5a809c3d6b31f664ec0"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb38929383fefe38909421eb0bf9d0ae058c541b93e80383b678755eede190"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:7f00ba54a5b44eeb6075005915a8a11c93ff04cf82ac620c7d33105cf4f7ad3e"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:7fd86318da844a9553e02d0d1dfdeb459b4d516c49c8ed66264ae59a40e3b7aa"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:b9dbdd0d9a7082d924489411bf84d8dd392f18bbfb4379a60fee97275e6d8c1f"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:689fed84ab320e428fb7ee1ee7ad5dae856f8d75c12df6860258fa94497eae7e"}, + {file = "fastar-0.12.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:70077a6d327ffa5d54f9d7bad6e6583c010530a5b120f5ad71a83b0a14d26ad9"}, + {file = "fastar-0.12.0.tar.gz", hash = "sha256:bba71522eae6a7627a5514ffdd4ac9645ef27d82e23931d79fd974bb49c3f2ad"}, +] + +[[package]] +name = "fastavro" +version = "1.12.2" +description = "Fast read/write of AVRO files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "fastavro-1.12.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7c6d26c731a0e1e8e7d4ae8f13ae524eb6ec0e90d99c8147a19fdbae14eb807"}, + {file = "fastavro-1.12.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7caeecf519eff50f007ca4bee16b6e0a8252e5fe682c94432192a20867239888"}, + {file = "fastavro-1.12.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:731aefe6c4bf2bafa0798ef83927676d06e44d1d18202cfb56d63b40422ab900"}, + {file = "fastavro-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f089f24225a28ddafa5cfad7c41cfa84db1a55f2d473370769a95c0e3bac60c9"}, + {file = "fastavro-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:653c4f90dd21d8a1e74309919e08934e420d9aef51d051d14bf5a1c0e8293c22"}, + {file = "fastavro-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:030f17eb4c7978538a31b55dea451ceace851a88dc9816b1923f8fb8a260db4c"}, + {file = "fastavro-1.12.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d48cd7094598a7e9d4297e8bf4bbe0dc9dc2ba4367d83dbb603e3b3c6aa35566"}, + {file = "fastavro-1.12.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:070c6134604bd7b6fd44409406ac50445339682b2e872885db2e859f92d22e93"}, + {file = "fastavro-1.12.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b73d50978d5e57416fa68461f9f3c8f39ea39e761cb1e12f919745adefe26a7"}, + {file = "fastavro-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c57a9920400166398695d92580eca21fd7a79f3c67d691ac7e20a7d1b5300735"}, + {file = "fastavro-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:81f6108f3ac292fb6cd05758c9e531389d8fc5e94e8c949b9298f4fb0a239662"}, + {file = "fastavro-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:eec44256856fd59d29d1f1d0950ace18a58e4228e7d49de5d5e1b1875b227dde"}, + {file = "fastavro-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:ecd1b23ea7f9af09c865ac8503d07afd7e6bf782d76bb83cbbdba15b7a0db807"}, + {file = "fastavro-1.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e331896e8efffc72fa03e63b87ebfc37960113127da8e0f5152d91664ffed68"}, + {file = "fastavro-1.12.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f01ebaada59d74fdf6d28e5031a961a413b3752e9edb0c03866fa18480cf4c8"}, + {file = "fastavro-1.12.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25ef6855935f67582740ffa6bb978e40ec51be876117a3555c36fa2488dcdf25"}, + {file = "fastavro-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84a4f76a0aece0aa72b5ed8162ba2ff8c78908b8361b5a5d92ddd161977ccb74"}, + {file = "fastavro-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e8da77d201916f6771fc357fda8267c2a256d7aa11923d43bc5f2fc155878b"}, + {file = "fastavro-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:1924349c74666c89417bd5cc2749f598e2f15f1d56ee81428b2317ab02c88aae"}, + {file = "fastavro-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:4c346cf449baf3b113e997c34151ad205e7135bc429469b005b180ade7e65e28"}, + {file = "fastavro-1.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:57bb6b908cb2e05baab63b04c3a31be3b4545a10bfab9748b8763016b5256704"}, + {file = "fastavro-1.12.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a007f95cc682f56e6d83f1d17c29c00bf719d6fe8e003282b535af3a1ba09c0"}, + {file = "fastavro-1.12.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e90460b0cd21f62be3cb26087e706e2cebb7b3fcef9e05b4473b61bb0415b5e"}, + {file = "fastavro-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ccd15966b8218d41b06ec3e7c2556be89a8a693026c771e6564d2e40bbaf8ea"}, + {file = "fastavro-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06b6971d3dae10cb34353b857d16ad21ebd6f0ea394e86c96abdcad109005d6e"}, + {file = "fastavro-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:98dfcdfaf1498ae2f0e2fafe900a82e8320cc81d8ae5a95b8b8879eaa3298c39"}, + {file = "fastavro-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:3888ef7a51adc77cdf07251bc762566a1be36211e1cff689f13980f3776a2f36"}, + {file = "fastavro-1.12.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:283dcd3129b632021894425974bedd0eb6db3bbf5994e448ccad10db4d803d31"}, + {file = "fastavro-1.12.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d125e210d5a0a1f701f12c0ecad9a03f1b04b5eddbce6ca36a1fc217da977ef"}, + {file = "fastavro-1.12.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d4d66afad78e8f47feaa307728a6b71fe3effc63ba2b9eeb109ee687c9bd397"}, + {file = "fastavro-1.12.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2328ec07925c04c89719e3971c9068a165c7fd474ea87675b1204de0440e71ff"}, + {file = "fastavro-1.12.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:55dea7e74b834d4b70467fc19c5b9ccb5509fe39abc4d26891187c1b22176423"}, + {file = "fastavro-1.12.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8d37c87826ae7195cfbd20fcd448801f2f563bb38f2691ec6574e39cb9eca6c8"}, + {file = "fastavro-1.12.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c463a3701f293e30d3d62e71e1989f112028d07f87432baf4507eeb57ec3831"}, + {file = "fastavro-1.12.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f604ba83498e209fff4c7ecc5063a39421dc538dace694bc592f9f338254f3dc"}, + {file = "fastavro-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bfac2dada8ddc002e8b7d8289d6fad4f070bc1fec20371cec684a7d10d932e96"}, + {file = "fastavro-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bc44ba6289fb1f5ee318335958dde6ad6d742dcb4bb8930de843e9024c64b68c"}, + {file = "fastavro-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:a475418f71c5aed69899813ecccf392429c08c3a63df3030129db71760b0db8f"}, + {file = "fastavro-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:daec9f9655a1d4636613c47d6d3343f6e039150d66cdce62543e20ca36612a8a"}, + {file = "fastavro-1.12.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:57594b72cf663bbd0f3ad8a319a999fc3d7c71065a6799b2c1d1a6a137894c5b"}, + {file = "fastavro-1.12.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74412132bbfb153cbf704517f2c89f7d3e170feb681b13bceace690f66f8d5fa"}, + {file = "fastavro-1.12.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e367a84c9133018e0a3bc822abe78d7f1f9a6092991a0ec409468cf4ef260282"}, + {file = "fastavro-1.12.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:044fafca0853e9ae14009de7763ac9e8e8f8b96f8a4e90bd58b695443266a370"}, + {file = "fastavro-1.12.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afede7324822800e4f90e96b9514188a237a60f35e8e7a10b2129c10c78f6e4d"}, + {file = "fastavro-1.12.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b5539711dfa1ec8f3eca57482b93a48a165af4a99e9d5f41e3af3fb913aadf92"}, + {file = "fastavro-1.12.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c01b0f0ce030a7b89263c0236ca77923eae352c5f35ecf214b04d3aaea8eb2c3"}, + {file = "fastavro-1.12.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:742d93f2ca835e4fa83a3ae9ed2bce8b28029ed62ac730f339a37685c23075cd"}, + {file = "fastavro-1.12.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ae60df21cc7059e2f3b1928ad2c0b75c6b26f9ada79d992f87b6fc3f50d3877e"}, + {file = "fastavro-1.12.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:03614131093a32c90c8fd95ff356c316752b8850cb8a770bc96cef17a003e2fc"}, + {file = "fastavro-1.12.2-cp39-cp39-win_amd64.whl", hash = "sha256:e235dfdabb51993bcd4a8f45c3a54f21a782f7f92b3def0648b0ace45a1b1ac7"}, + {file = "fastavro-1.12.2.tar.gz", hash = "sha256:3c79502d56cf6b76210032e1c53494ddfbc73c140bccf2ef4092b3f0825323ab"}, +] + +[package.extras] +codecs = ["backports.zstd", "cramjam", "lz4"] +lz4 = ["lz4"] +snappy = ["cramjam"] +zstandard = ["backports.zstd"] + +[[package]] +name = "fastmcp" +version = "4.0.10" +description = "The fast, Pythonic way to build MCP servers and clients." +optional = false +python-versions = ">=3.10" +files = [ + {file = "fastmcp-4.0.10-py3-none-any.whl", hash = "sha256:1f8462e3d97394a637e1b0f1eaea0002da661c8d98000ad43f0aa3fa1ff523e8"}, + {file = "fastmcp-4.0.10.tar.gz", hash = "sha256:64539631a7dc5873a1c7a44089b2f7b3395dea3ca857e9f81bfe9665a59038d5"}, +] + +[package.dependencies] +fastmcp-slim = {version = "4.0.10", extras = ["client", "server"]} + +[package.extras] +anthropic = ["fastmcp-slim[anthropic] (==4.0.10)"] +apps = ["fastmcp-slim[apps] (==4.0.10)"] +azure = ["fastmcp-slim[azure] (==4.0.10)"] +code-mode = ["fastmcp-slim[code-mode] (==4.0.10)"] +gemini = ["fastmcp-slim[gemini] (==4.0.10)"] +jev = ["fastmcp-slim[jev] (==4.0.10)"] +openai = ["fastmcp-slim[openai] (==4.0.10)"] +tasks = ["fastmcp-tasks (==4.0.10)"] + +[[package]] +name = "fastmcp-slim" +version = "4.0.10" +description = "The dependency-slim FastMCP package." +optional = false +python-versions = ">=3.10" +files = [ + {file = "fastmcp_slim-4.0.10-py3-none-any.whl", hash = "sha256:c2abd40302b8f06b0291413cf537d57bc0f171b5fd98af5cfef3b2b6b506122f"}, + {file = "fastmcp_slim-4.0.10.tar.gz", hash = "sha256:d22b423f93ad945bf65d446a1689ff84be7b91dfd9b6f23c5013f4b2fe9776a7"}, +] + +[package.dependencies] +authlib = {version = ">=1.6.11", optional = true, markers = "extra == \"client\" or extra == \"server\""} +cyclopts = {version = ">=4.0.0", optional = true, markers = "extra == \"server\""} +exceptiongroup = {version = ">=1.2.2", optional = true, markers = "extra == \"client\" or extra == \"server\""} +griffelib = {version = ">=2.0.0", optional = true, markers = "extra == \"server\""} +httpx2 = {version = ">=2.5.0", optional = true, markers = "extra == \"client\" or extra == \"server\""} +joserfc = {version = ">=1.5.0", optional = true, markers = "extra == \"server\""} +jsonref = {version = ">=1.1.0", optional = true, markers = "extra == \"server\""} +jsonschema-path = {version = ">=0.3.4", optional = true, markers = "extra == \"server\""} +mcp = {version = ">=2.0.0,<3.0.0", optional = true, markers = "extra == \"client\" or extra == \"server\""} +mcp-types = ">=2.0.0,<3.0.0" +openapi-pydantic = {version = ">=0.5.1", optional = true, markers = "extra == \"server\""} +opentelemetry-api = {version = ">=1.28.0", optional = true, markers = "extra == \"client\" or extra == \"server\""} +packaging = {version = ">=24.0", optional = true, markers = "extra == \"server\""} +platformdirs = ">=4.0.0" +py-key-value-aio = {version = ">=0.4.4,<0.5.0", extras = ["filetree", "keyring", "memory"], optional = true, markers = "extra == \"client\" or extra == \"server\""} +pydantic = {version = ">=2.12.0", extras = ["email"]} +pydantic-settings = ">=2.0.0" +pyperclip = {version = ">=1.9.0", optional = true, markers = "extra == \"server\""} +python-dotenv = ">=1.1.0" +python-multipart = {version = ">=0.0.26", optional = true, markers = "extra == \"server\""} +pyyaml = {version = ">=6.0,<7.0", optional = true, markers = "extra == \"server\""} +rich = ">=13.9.4" +starlette = {version = ">=1.0.1", optional = true, markers = "extra == \"client\" or extra == \"server\""} +typing-extensions = ">=4.0.0" +uncalled-for = {version = ">=0.4.0", optional = true, markers = "extra == \"server\""} +uvicorn = {version = ">=0.35", optional = true, markers = "extra == \"server\""} +watchfiles = {version = ">=1.0.0", optional = true, markers = "extra == \"server\""} +websockets = {version = ">=15.0.1", optional = true, markers = "extra == \"server\""} + +[package.extras] +anthropic = ["anthropic (>=0.48.0,<2)"] +apps = ["prefab-ui (>=0.18.0)"] +azure = ["azure-identity (>=1.16.0)", "pyjwt (>=2.12.0)"] +client = ["authlib (>=1.6.11)", "exceptiongroup (>=1.2.2)", "httpx2 (>=2.5.0)", "mcp (>=2.0.0,<3.0.0)", "opentelemetry-api (>=1.28.0)", "py-key-value-aio[filetree,keyring,memory] (>=0.4.4,<0.5.0)", "starlette (>=1.0.1)"] +code-mode = ["pydantic-monty (==0.0.21)"] +gemini = ["google-genai (>=1.18.0)", "jsonref (>=1.1.0)"] +jev = ["typesafe-sdk (>=0.5.7)"] +mcp = ["exceptiongroup (>=1.2.2)", "httpx2 (>=2.5.0)", "mcp (>=2.0.0,<3.0.0)", "opentelemetry-api (>=1.28.0)", "starlette (>=1.0.1)"] +openai = ["openai (>=1.102.0)"] +server = ["authlib (>=1.6.11)", "cyclopts (>=4.0.0)", "exceptiongroup (>=1.2.2)", "griffelib (>=2.0.0)", "httpx2 (>=2.5.0)", "joserfc (>=1.5.0)", "jsonref (>=1.1.0)", "jsonschema-path (>=0.3.4)", "mcp (>=2.0.0,<3.0.0)", "openapi-pydantic (>=0.5.1)", "opentelemetry-api (>=1.28.0)", "packaging (>=24.0)", "py-key-value-aio[filetree,keyring,memory] (>=0.4.4,<0.5.0)", "pyperclip (>=1.9.0)", "python-multipart (>=0.0.26)", "pyyaml (>=6.0,<7.0)", "starlette (>=1.0.1)", "uncalled-for (>=0.4.0)", "uvicorn (>=0.35)", "watchfiles (>=1.0.0)", "websockets (>=15.0.1)"] + +[[package]] +name = "fastuuid" +version = "0.14.0" +description = "Python bindings to Rust's UUID library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a"}, + {file = "fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00"}, + {file = "fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8"}, + {file = "fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe"}, + {file = "fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d"}, + {file = "fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a"}, + {file = "fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7"}, + {file = "fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8"}, + {file = "fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36"}, + {file = "fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94"}, + {file = "fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24"}, + {file = "fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d"}, + {file = "fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09"}, + {file = "fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057"}, + {file = "fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8"}, + {file = "fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176"}, + {file = "fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc"}, + {file = "fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87"}, + {file = "fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995"}, + {file = "fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab"}, + {file = "fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad"}, + {file = "fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b"}, + {file = "fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f"}, + {file = "fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad"}, + {file = "fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8"}, + {file = "fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06"}, + {file = "fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:47c821f2dfe95909ead0085d4cb18d5149bca704a2b03e03fb3f81a5202d8cea"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3964bab460c528692c70ab6b2e469dd7a7b152fbe8c18616c58d34c93a6cf8d4"}, + {file = "fastuuid-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c501561e025b7aea3508719c5801c360c711d5218fc4ad5d77bf1c37c1a75779"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dce5d0756f046fa792a40763f36accd7e466525c5710d2195a038f93ff96346"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193ca10ff553cf3cc461572da83b5780fc0e3eea28659c16f89ae5202f3958d4"}, + {file = "fastuuid-0.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0737606764b29785566f968bd8005eace73d3666bd0862f33a760796e26d1ede"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0976c0dff7e222513d206e06341503f07423aceb1db0b83ff6851c008ceee06"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6fbc49a86173e7f074b1a9ec8cf12ca0d54d8070a85a06ebf0e76c309b84f0d0"}, + {file = "fastuuid-0.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:de01280eabcd82f7542828ecd67ebf1551d37203ecdfd7ab1f2e534edb78d505"}, + {file = "fastuuid-0.14.0-cp38-cp38-win32.whl", hash = "sha256:af5967c666b7d6a377098849b07f83462c4fedbafcf8eb8bc8ff05dcbe8aa209"}, + {file = "fastuuid-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3091e63acf42f56a6f74dc65cfdb6f99bfc79b5913c8a9ac498eb7ca09770a8"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2ec3d94e13712a133137b2805073b65ecef4a47217d5bac15d8ac62376cefdb4"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:139d7ff12bb400b4a0c76be64c28cbe2e2edf60b09826cbfd85f33ed3d0bbe8b"}, + {file = "fastuuid-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d55b7e96531216fc4f071909e33e35e5bfa47962ae67d9e84b00a04d6e8b7173"}, + {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0eb25f0fd935e376ac4334927a59e7c823b36062080e2e13acbaf2af15db836"}, + {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089c18018fdbdda88a6dafd7d139f8703a1e7c799618e33ea25eb52503d28a11"}, {file = "fastuuid-0.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fc37479517d4d70c08696960fad85494a8a7a0af4e93e9a00af04d74c59f9e3"}, {file = "fastuuid-0.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:73657c9f778aba530bc96a943d30e1a7c80edb8278df77894fe9457540df4f85"}, {file = "fastuuid-0.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d31f8c257046b5617fc6af9c69be066d2412bdef1edaa4bdf6a214cf57806105"}, @@ -925,13 +1832,23 @@ files = [ [[package]] name = "filelock" -version = "3.32.5" +version = "4.0.3" description = "A platform independent file lock." optional = false python-versions = ">=3.10" files = [ - {file = "filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2"}, - {file = "filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d"}, + {file = "filelock-4.0.3-py3-none-any.whl", hash = "sha256:30cd166e2aee2c7534ce2c33c6367c4cd051b8368e20e2c4eb0c34f699bacfab"}, + {file = "filelock-4.0.3.tar.gz", hash = "sha256:87296d60478e14204fd9406e79831400fef76693bae2895deec236c98e87a8aa"}, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +description = "The FlatBuffers serialization format for Python" +optional = false +python-versions = "*" +files = [ + {file = "flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4"}, ] [[package]] @@ -1075,13 +1992,13 @@ files = [ [[package]] name = "fsspec" -version = "2026.7.0" +version = "2026.9.0" description = "File-system specification" optional = false python-versions = ">=3.10" files = [ - {file = "fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279"}, - {file = "fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88"}, + {file = "fsspec-2026.9.0-py3-none-any.whl", hash = "sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f"}, + {file = "fsspec-2026.9.0.tar.gz", hash = "sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe"}, ] [package.extras] @@ -1108,7 +2025,7 @@ sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] +test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray", "zarr"] test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs (>=2026.4.0)", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "s3fs (>=2026.6.0)", "smbprotocol", "tqdm", "urllib3", "zarr (<3.2.0)", "zstandard"] tqdm = ["tqdm"] @@ -1146,15 +2063,33 @@ files = [ [package.dependencies] smmap = ">=3.0.1,<6" +[[package]] +name = "github-copilot-sdk" +version = "1.0.14" +description = "Python SDK for GitHub Copilot CLI" +optional = false +python-versions = ">=3.11" +files = [ + {file = "github_copilot_sdk-1.0.14-py3-none-any.whl", hash = "sha256:9f4538cec7295f24507c650d794307396458a51f280a0fbbcd840da177109050"}, +] + +[package.dependencies] +httpx = ">=0.24.0" +pydantic = ">=2.0" +python-dateutil = ">=2.9.0.post0" + +[package.extras] +telemetry = ["opentelemetry-api (>=1.0.0)"] + [[package]] name = "gitpython" -version = "3.1.61" +version = "3.1.62" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "gitpython-3.1.61-py3-none-any.whl", hash = "sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c"}, - {file = "gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95"}, + {file = "gitpython-3.1.62-py3-none-any.whl", hash = "sha256:7002251225e10e29d2e1f49e6532613fe5d5d9f0b6f1f02997a52b38fe56899e"}, + {file = "gitpython-3.1.62.tar.gz", hash = "sha256:1791de66309bc0c7cfca40bf8d2e3de7ca091cbf94e6051be1ad0722c61062af"}, ] [package.dependencies] @@ -1164,6 +2099,160 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["basedpyright (==1.39.9)", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy (==1.18.2)", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] +[[package]] +name = "google-auth" +version = "2.58.1" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.10" +files = [ + {file = "google_auth-2.58.1-py3-none-any.whl", hash = "sha256:66a63572d0ab629554768544df04018d79178c0bd7439ba31683ac576b79024d"}, + {file = "google_auth-2.58.1.tar.gz", hash = "sha256:1480461d8b2347e679af758f4003700ea79a59b603c9134e18bfa95e8004ee70"}, +] + +[package.dependencies] +cryptography = {version = ">=38.0.3", markers = "python_version < \"3.14\""} +pyasn1-modules = ">=0.2.1" +requests = {version = ">=2.30.0,<3.0.0", optional = true, markers = "extra == \"requests\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.8.0,<4.0.0)", "aiohttp (>=3.9.0,<4.0.0)", "requests (>=2.30.0,<3.0.0)"] +cryptography = ["cryptography (>=38.0.3)", "cryptography (>=41.0.5)"] +enterprise-cert = ["cryptography (>=38.0.3)", "cryptography (>=41.0.5)"] +grpc = ["grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)"] +pyjwt = ["pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "cryptography (>=41.0.5)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.30.0,<3.0.0)"] +rsa = ["rsa (>=4.0.0,<5)"] +testing = ["aiohttp (>=3.8.0,<4.0.0)", "aiohttp (>=3.9.0,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "packaging (>=20.0)", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.30.0,<3.0.0)", "responses", "urllib3 (>=1.26.15,<3.0.0)"] +urllib3 = ["packaging (>=20.0)", "urllib3 (>=1.26.15,<3.0.0)"] + +[[package]] +name = "google-genai" +version = "2.25.0" +description = "GenAI Python SDK" +optional = false +python-versions = ">=3.10" +files = [ + {file = "google_genai-2.25.0-py3-none-any.whl", hash = "sha256:2e8f3a5f76ed00d5ffc3153f6089bc7d3511054eed53ad9cfc6095a7dc59b028"}, + {file = "google_genai-2.25.0.tar.gz", hash = "sha256:ab603baa5eee0205926ad0f8d7f93e0400df6d67650e99c33e01ab228ea16ad6"}, +] + +[package.dependencies] +anyio = ">=4.8.0,<5.0.0" +distro = ">=1.7.0,<2" +google-auth = {version = ">=2.56.0,<3.0.0", extras = ["requests"]} +httpx = ">=0.28.1,<1.0.0" +pydantic = ">=2.12.5,<3.0.0" +requests = ">=2.28.1,<3.0.0" +sniffio = "*" +tenacity = ">=8.2.3,<9.2.0" +typing-extensions = ">=4.14.0,<5.0.0" +websockets = ">=13.0.0,<17.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.10.11,<4.0.0)"] +local-tokenizer = ["pillow", "protobuf", "sentencepiece (>=0.2.0)", "torch", "torchvision", "transformers"] +pyopenssl = ["pyopenssl"] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.4" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.10" +files = [ + {file = "googleapis_common_protos-1.75.4-py3-none-any.whl", hash = "sha256:e8eb9cffa9a9f3423090cc7a086012004196abab388a0561690e81887fd79cc1"}, + {file = "googleapis_common_protos-1.75.4.tar.gz", hash = "sha256:4587babdc82a8d7e5a3d4f5a6697e064bf44a598b4d08341c212b68185eadbcd"}, +] + +[package.dependencies] +protobuf = ">=6.33.5,<8.0.0" + +[package.extras] +grpc = ["grpcio (>=1.59.0,<2.0.0)"] + +[[package]] +name = "greenlet" +version = "3.3.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +files = [ + {file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"}, + {file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"}, + {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, + {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, + {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, + {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, + {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, + {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, + {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, + {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, + {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, + {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, + {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, + {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, + {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, + {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, + {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "griffelib" +version = "2.3.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.10" +files = [ + {file = "griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0"}, + {file = "griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115"}, +] + +[package.extras] +pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"] + [[package]] name = "h11" version = "0.16.0" @@ -1204,6 +2293,84 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "hindsight-api-slim" +version = "0.10.1" +description = "Hindsight: Agent Memory That Learns" +optional = false +python-versions = ">=3.11" +files = [ + {file = "hindsight_api_slim-0.10.1-py3-none-any.whl", hash = "sha256:998dfe11ed8685311f7f1aaa06c97fdf6cc3a4289b2666b6f526a6e1f0905aa1"}, + {file = "hindsight_api_slim-0.10.1.tar.gz", hash = "sha256:8bdc5a484f2e86a827354d941d6a164fb74849690ca7ecb98751637075c3997e"}, +] + +[package.dependencies] +aiohttp = ">=3.14.3" +alembic = ">=1.17.1" +anthropic = ">=0.40.0" +asyncpg = ">=0.30.0" +authlib = ">=1.6.9" +boto3 = ">=1.42.74" +claude-agent-sdk = ">=0.2.82" +cohere = ">=5.0.0" +croniter = ">=2.0.0" +cryptography = ">=50.0.0" +dateparser = ">=1.4.2" +fastapi = {version = ">=0.120.3", extras = ["standard"]} +fastmcp = ">=3.2.0" +filelock = ">=3.20.1" +github-copilot-sdk = ">=1.0.11" +google-auth = ">=2.0.0" +google-genai = ">=1.72.0" +greenlet = ">=3.2.4,<3.4.0" +httpx = ">=0.27.0" +json-repair = ">=0.63.2" +litellm = [ + {version = ">=1.91.3,<1.92", markers = "sys_platform == \"darwin\""}, + {version = ">=1.93.0", markers = "sys_platform != \"darwin\""}, +] +markitdown = {version = ">=0.1.4", extras = ["docx", "pdf", "pptx", "xls", "xlsx"]} +numpy = ">=1.26.0" +obstore = ">=0.4.0" +openai = ">=1.66.0" +opentelemetry-api = ">=1.44.0" +opentelemetry-exporter-otlp-proto-http = ">=1.44.0" +opentelemetry-exporter-prometheus = ">=0.65b0" +opentelemetry-instrumentation-fastapi = ">=0.65b0" +opentelemetry-sdk = ">=1.44.0" +opentelemetry-semantic-conventions = ">=0.65b0" +orjson = ">=3.11.0" +pgvector = ">=0.4.1" +pillow = ">=12.3.0" +protobuf = ">=7.35.1" +psycopg2-binary = ">=2.9.11" +pyasn1 = ">=0.6.3" +pydantic = ">=2.0.0" +pygments = ">=2.20.0" +pyjwt = ">=2.12.0" +python-dateutil = ">=2.8.0" +python-dotenv = ">=1.0.0" +python-multipart = ">=0.0.22" +regex = ">=2026.9.3" +rich = ">=13.0.0" +sqlalchemy = ">=2.0.44,<2.1" +toktok-rs = ">=0.1.3" +typer = ">=0.9.0" +urllib3 = ">=2.7.0" +uvicorn = ">=0.38.0" +uvloop = {version = ">=0.22.1", markers = "sys_platform != \"win32\""} +winloop = {version = ">=0.1.0", markers = "sys_platform == \"win32\""} +wsproto = ">=1.0.0" + +[package.extras] +all = ["einops (>=0.8.2)", "flashrank (>=0.2.0)", "huggingface-hub (>=0.20.0)", "mlx (>=0.31.0)", "mlx-lm (>=0.31.1)", "numpy (>=1.26.0)", "onnxruntime (>=1.17.0)", "pg0-embedded (>=0.15.0)", "safetensors (>=0.6.2)", "sentence-transformers (>=5.0.0)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.11.0)", "transformers (>=5.5.0)"] +embedded-db = ["pg0-embedded (>=0.15.0)"] +local-llm = ["huggingface-hub (>=0.20.0)", "llama-cpp-python[server] (>=0.3.0)"] +local-ml = ["einops (>=0.8.2)", "flashrank (>=0.2.0)", "mlx (>=0.31.0)", "mlx-lm (>=0.31.1)", "safetensors (>=0.6.2)", "sentence-transformers (>=5.0.0)", "tokenizers (>=0.22.0,<=0.23.0)", "torch (>=2.11.0)", "transformers (>=5.5.0)"] +local-onnx = ["huggingface-hub (>=0.20.0)", "numpy (>=1.26.0)", "onnxruntime (>=1.17.0)", "tokenizers (>=0.22.0,<=0.23.0)", "transformers (>=5.5.0)"] +oracle = ["oracledb (>=2.5.0)"] +test = ["filelock (>=3.20.1)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.21.0)", "pytest-timeout (>=2.4.0)", "pytest-xdist (>=3.0.0)", "testcontainers (>=4.0.0)"] + [[package]] name = "httpcore" version = "1.0.9" @@ -1225,6 +2392,86 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<1.0)"] +[[package]] +name = "httpcore2" +version = "2.13.1" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.10" +files = [ + {file = "httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a"}, + {file = "httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103"}, +] + +[package.dependencies] +h11 = ">=0.16" +truststore = ">=0.10" + +[package.extras] +asyncio = ["anyio (>=4.5.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.34.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.8.0" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.9" +files = [ + {file = "httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826"}, + {file = "httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba"}, + {file = "httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124"}, + {file = "httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745"}, + {file = "httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683"}, + {file = "httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0"}, + {file = "httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081"}, + {file = "httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247"}, + {file = "httptools-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62"}, + {file = "httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999"}, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1250,32 +2497,59 @@ socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] [[package]] -name = "httpx-sse" -version = "0.4.3" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." +name = "httpx2" +version = "2.13.1" +description = "The next generation HTTP client." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" +files = [ + {file = "httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4"}, + {file = "httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa"}, +] + +[package.dependencies] +anyio = {version = ">=4.10", markers = "sys_platform != \"emscripten\""} +httpcore2 = {version = "2.13.1", markers = "sys_platform != \"emscripten\""} +httpx2-jsfetch = {version = "*", markers = "sys_platform == \"emscripten\" and python_version >= \"3.12\""} +idna = ">=3.18" +truststore = {version = ">=0.10", markers = "sys_platform != \"emscripten\""} +typing-extensions = {version = ">=4.5.0", markers = "python_version < \"3.13\""} + +[package.extras] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.2)"] +cli = ["click (>=8.4.2)", "pygments (==2.*)", "rich (>=10,<16)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +ws = ["wsproto (>=1.2)"] +zstd = ["backports-zstd (>=1.0.0)"] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +description = "httpx2 transports for Emscripten/Pyodide, backed by the JavaScript fetch API." +optional = false +python-versions = ">=3.12" files = [ - {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, - {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, + {file = "httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32"}, + {file = "httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60"}, ] [[package]] name = "huggingface-hub" -version = "1.29.0" +version = "1.33.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.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd"}, - {file = "huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad"}, + {file = "huggingface_hub-1.33.0-py3-none-any.whl", hash = "sha256:04e434b06e100eddbce9a6e817d72693a7884b10a79bd67ab48080d5c07eb899"}, + {file = "huggingface_hub-1.33.0.tar.gz", hash = "sha256:367be21a201db9523eddf8aeac7048f2602c1b308691c97640d5e72ed188007e"}, ] [package.dependencies] click = ">=8.4.2,<9.0.0" filelock = ">=3.10.0" fsspec = ">=2023.5.0" -hf-xet = {version = ">=1.5.2,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} +hf-xet = {version = ">=1.6.0,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} httpx = ">=0.23.0,<1" packaging = ">=20.9" pyyaml = ">=5.1" @@ -1287,23 +2561,37 @@ all = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", dev = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastapi", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "numpy", "pytest (>=8.4.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures (>=16.2)", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "ty", "types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] gradio = ["gradio (>=5.0.0)", "requests"] -hf-xet = ["hf-xet (>=1.5.2,<2.0.0)"] -mcp = ["mcp (>=1.8.0,<2.0.0)"] +hf-xet = ["hf-xet (>=1.6.0,<2.0.0)"] +mcp = ["mcp (>=1.9.4,<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 = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + [[package]] name = "idna" -version = "3.19" +version = "3.20" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" files = [ - {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, - {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, + {file = "idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c"}, + {file = "idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44"}, ] [package.extras] @@ -1343,6 +2631,83 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535"}, + {file = "jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30"}, + {file = "jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + [[package]] name = "jinja2" version = "3.1.6" @@ -1362,116 +2727,130 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jiter" -version = "0.16.0" +version = "0.17.0" description = "Fast iterable JSON parser." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c"}, - {file = "jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244"}, - {file = "jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f"}, - {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131"}, - {file = "jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b"}, - {file = "jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9"}, - {file = "jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26"}, - {file = "jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3"}, - {file = "jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03"}, - {file = "jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea"}, - {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91"}, - {file = "jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3"}, - {file = "jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7"}, - {file = "jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1"}, - {file = "jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056"}, - {file = "jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a"}, - {file = "jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9"}, - {file = "jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5"}, - {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730"}, - {file = "jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f"}, - {file = "jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274"}, - {file = "jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7"}, - {file = "jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331"}, - {file = "jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195"}, - {file = "jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053"}, - {file = "jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e"}, - {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb"}, - {file = "jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84"}, - {file = "jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e"}, - {file = "jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd"}, - {file = "jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a"}, - {file = "jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee"}, - {file = "jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29"}, - {file = "jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93"}, - {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a"}, - {file = "jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00"}, - {file = "jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe"}, - {file = "jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106"}, - {file = "jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8"}, - {file = "jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585"}, - {file = "jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af"}, - {file = "jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e"}, - {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077"}, - {file = "jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734"}, - {file = "jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf"}, - {file = "jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db"}, - {file = "jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce"}, - {file = "jiter-0.16.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d8f80521644426d451e70f00c7974240cab8f6ee088aedaa9af2697153ab7805"}, - {file = "jiter-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b21b412b899fd8bd51a3046934b59a3bb068b79f70a5c6010053ac77cc53f0c"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0758ab7747a984797cf048e8eedea1d8ef39d7994b25611daf5b48fc903e8873"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ec553a99b0987efd7a3645a1a825cf29c224e494db267a83369fcc8da9aeda5"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3bd327cdfa118bc1ce69c214c2678571d5bd39b8ccd0ebf43a54db00541ba9a"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26d122613ada2b708eb714695446f40fce5bdf2edb4b02116dec62faa62dfab3"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e03a5f21a5ce96a9441b8cb32719a8b88ed5388f53e0f339c5bcf54f1317f9d0"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:a5c54ef4ff776d9675837ef535b3308d6e31c208d43ebc44a0f7ab8a208c68f7"}, - {file = "jiter-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1e7923093a376d93c6eb507c77045ae258d689ba577392846a1b3f10d0b09a9"}, - {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2a0d46ef67cc58d906a6132dd3040ca70ae4f0b0d7c9c052fe432c658a69b3f6"}, - {file = "jiter-0.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:70a490b55634dc0d2606ce8a8e01b1d62459011beb368d15d76e1eaf62460e3d"}, - {file = "jiter-0.16.0-cp39-cp39-win32.whl", hash = "sha256:9acf1b2faec82d998811ecce7ae84d9005e53410773e9d37d61cdc424ba4581b"}, - {file = "jiter-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:491e7d072a253b156fff46b78bceac4652a697aa8d7082c9c18c03d7b7917d24"}, - {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad"}, - {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f"}, - {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5"}, - {file = "jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85"}, - {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127"}, - {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3"}, - {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702"}, - {file = "jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2"}, - {file = "jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c"}, + {file = "jiter-0.17.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ed1a24005daac667d577402d75a2922f9775a165b146b883ff1ad3602d8be689"}, + {file = "jiter-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b847b18d066c46b3b7ae49d6c94a7634c5e4a8983146ee25562a092000f5e3ad"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b68d3495d95da120651a5628c7ebadee84ed001a1b76e6afc325c42482f15b5"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c1a5336c04a41b1f1cf9572e294aec27cc569767ff73de7bf87a91f0bea7cb9"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b75f85660108965a94be77911a25a253429307294d9415b3c597118977a614de"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32aaaa764604496610a3ad2d98503ae88ccb2fbe769e892ff4533e778e85f708"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:826871c42cebaae22f0a2b5673a4a1a75c851bb2d13b3c17764a630a6b298984"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:00b5a98df3e3a3e8cf7b619f4ac2f8bf975bbf3d95d02c5d17b8dbfe5c8b8245"}, + {file = "jiter-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6af5b74073bd25bae695e6d00919f6a9be7ed5a9f8836d981eb1ffe84139e6fb"}, + {file = "jiter-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:16dd0c1baf098ae70b8f3616574eb3fedf34e26670b89e16a7e67561f737ed2d"}, + {file = "jiter-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:545c36a0f3b2238c242cc9785439d3242a871b7bc39fe3f441bcaa07bf3aa83e"}, + {file = "jiter-0.17.0-cp310-cp310-win32.whl", hash = "sha256:155be7355bdb7ca76ab0961be8982c225f964a5c073a83984183f22391cc29fc"}, + {file = "jiter-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:37150a9e02e869475854fa20b7d0d5e26d18d0f8bc17293999973ff27e99ae7a"}, + {file = "jiter-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cfafd7be8b16ceadd298db542cead37cddc211c4c49e04ad2596924df18625b1"}, + {file = "jiter-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8adca2e793288e5f1bb29279bb439d0d3cfbb50eddca7e7e6ffd42ff4f482406"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c692d567ba206c7cca38c9d1d0ccc70c9786290173c184d871ca12e9981ed7"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81c83c0abe614446a283d994d2c07c4f58632dea2cdf66ba9e2921bb8ccd593e"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:073dc68c1a700c8fc480e877864a6b6ffc887533e261f4380c08c16bf09d057a"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:492f37230bbf9581ab2c17bcda862c249afb9ae2e3ab2dd6db59943bc4cc3153"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5888fe5abc1ca2fa834a3e1b4c7ef0dcece286a7d7e95a609ef0934b777b9fc9"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:84ac78df457e1ee3f7e733bd114823302ae8c5ad5542d7e6647d92ffaa090a04"}, + {file = "jiter-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7573e80232c5bcf80c24c038cf7e53a463f5c3b1dd1dd4109d66304f4dccc233"}, + {file = "jiter-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:11902505d401691720f5785c15b02204248526edee11b635cd6c40cd52b81599"}, + {file = "jiter-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:64846211a2debe7c071d2146d2283d2b0c1c93dc8fd5fb7794faac2ca6061b5c"}, + {file = "jiter-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c19b9357309b8cc6de8a48fca8e44a8c9c2feaaa2f5896d037fa505d48fcab80"}, + {file = "jiter-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:e654b6b04e39c9cb19cb8b04c6ddf1f2db07751fa14156413969fd78bad0e5cb"}, + {file = "jiter-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:3ad556afc289f15d2b181b941982d01f06190863c07440185b9f354e1bd2def3"}, + {file = "jiter-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91"}, + {file = "jiter-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812"}, + {file = "jiter-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a"}, + {file = "jiter-0.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44"}, + {file = "jiter-0.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31"}, + {file = "jiter-0.17.0-cp312-cp312-win32.whl", hash = "sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd"}, + {file = "jiter-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3"}, + {file = "jiter-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8"}, + {file = "jiter-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa"}, + {file = "jiter-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca"}, + {file = "jiter-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8"}, + {file = "jiter-0.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9"}, + {file = "jiter-0.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8"}, + {file = "jiter-0.17.0-cp313-cp313-win32.whl", hash = "sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7"}, + {file = "jiter-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd"}, + {file = "jiter-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac"}, + {file = "jiter-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5"}, + {file = "jiter-0.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b"}, + {file = "jiter-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b"}, + {file = "jiter-0.17.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9"}, + {file = "jiter-0.17.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f"}, + {file = "jiter-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03"}, + {file = "jiter-0.17.0-cp314-cp314-win32.whl", hash = "sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8"}, + {file = "jiter-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312"}, + {file = "jiter-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744"}, + {file = "jiter-0.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768"}, + {file = "jiter-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718"}, + {file = "jiter-0.17.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59"}, + {file = "jiter-0.17.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1"}, + {file = "jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06"}, + {file = "jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9"}, + {file = "jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc"}, + {file = "jiter-0.17.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:84963d3f395ef5e9a32ce47155e08a7962fa292c159a10cb98b931cef1416925"}, + {file = "jiter-0.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ffa0380ad091de7d3fc33e17a97ff479851ee18a0a2a3ee56ff3215cdc886656"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:755079792868ce5d4938e83b91a0939b34fb858a1ca65a104f2d771bea57faa1"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3bf4dc2b84a464117fb097d15a25c58d100d2692888e3b0d92df5b48ed16b7c0"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02a360707033d8cef53f7f3480817a1489177a259ec6ec01e98c37e0b922ddca"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:300ce01ab0215e3dea4d00090143c909aedc65c0f809b3c07983e1d038f291b9"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746243a080b4ca790b8499af3d7cf9825d5f5987933950cd818e767ee353d826"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b550585523339b71cb852b811aae49d08d7601ad8ffe9f5dc1562f4c3d22fd87"}, + {file = "jiter-0.17.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0239520085cac678e77a606fd7e3f1c60c371d719790c5e3807388d3da4354c2"}, + {file = "jiter-0.17.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:eb2295da7c3769f6719b227a237aa6a5cfa6550e478bc838001b592c57e16575"}, + {file = "jiter-0.17.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:e088612ff90ebc9247e1a43074b72835804261c47e6a6c01cb3ddcb55360d688"}, + {file = "jiter-0.17.0-cp315-cp315-win32.whl", hash = "sha256:0b52d52035b3907c5b1f6277857b29c1cbfc965e24e0f27330dbed83edb591ec"}, + {file = "jiter-0.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:10f5558eed511b830488003449d942bd75829ad6257dc58cb9a03e596a7777b1"}, + {file = "jiter-0.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:fa13acf1046f95df808c64b1310705e143fab87aee73ae00cc42d640867fd2c1"}, + {file = "jiter-0.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:af2f7501580f274b63c4b2283bc425f5df7edf06ae5b171e5f87d912ff359a20"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c5349312e5cb02b7a21e123a57665afa895953f05bf252a9dd4c13a572b7ab"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86f3f9343a288eb85a81ef20a752b2f84564296636db54a9fff0b5c8deaf1df2"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4607ec7d93355fbc25b8dc5189153cf21d66063b9f9cd04dd2774e6e783f9b6a"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10cd64a5720ad7f809ac5466ff1705813f1b6b510f195a73acafba0ac0e1f675"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe9f61bb30174d2f5c8396445c360c96c44e78164d0815dfe627ccf57849574"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:370d8fe5bf201dc6925e8a84c81ac7291f74d9fd1778234fc79d517064a5c76b"}, + {file = "jiter-0.17.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b303d88e6a0bda789ec4b7801c7bad68e27230ba1fe4baffc756d1fbd32dc9d"}, + {file = "jiter-0.17.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:30793a24a31e968969757c9e08d830cbb15a2cd3c4959b4498b38f4b1c2258eb"}, + {file = "jiter-0.17.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:686c93d86f2b426c803024b805bd161a6cd10e9627c23e901640eab646c0ad8a"}, + {file = "jiter-0.17.0-cp315-cp315t-win32.whl", hash = "sha256:86d703d9faa1ffc8ae4e9de0fa007712ed2171b5c0d93811a8e2e105ac729b0d"}, + {file = "jiter-0.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:42b0260445251b1bc520a63baa94a32d88e0f931fba234f1764db7feb7c72174"}, + {file = "jiter-0.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23"}, + {file = "jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33"}, + {file = "jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2"}, + {file = "jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39"}, + {file = "jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29f49b325e0234e4ad9ecca5b861ffbd09b95ccac9bd46fa55841b6e56eea5fe"}, + {file = "jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c"}, + {file = "jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2"}, + {file = "jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5"}, + {file = "jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff"}, + {file = "jiter-0.17.0.tar.gz", hash = "sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12"}, ] [[package]] @@ -1485,20 +2864,48 @@ files = [ {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, ] +[[package]] +name = "joserfc" +version = "1.7.5" +description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" +optional = false +python-versions = ">=3.10" +files = [ + {file = "joserfc-1.7.5-py3-none-any.whl", hash = "sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159"}, + {file = "joserfc-1.7.5.tar.gz", hash = "sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea"}, +] + +[package.dependencies] +cryptography = ">=45.0.1" + +[package.extras] +drafts = ["pycryptodome"] + [[package]] name = "json-repair" -version = "0.63.4" +version = "0.63.5" description = "A package to repair broken json strings" optional = false python-versions = ">=3.10" files = [ - {file = "json_repair-0.63.4-py3-none-any.whl", hash = "sha256:0f374f3eee21454aef0a5d72c06b8689b660a1788f80ab392639e3f7d5c5d458"}, - {file = "json_repair-0.63.4.tar.gz", hash = "sha256:77aa642193d62b02b889e8ce0df33898d3ea87282f0b9d8653f8ce8772c642b4"}, + {file = "json_repair-0.63.5-py3-none-any.whl", hash = "sha256:0e39b3066f7829b4bd7fc614e463cfd4ff7a4c46f82f5b65431fc11e29a74c81"}, + {file = "json_repair-0.63.5.tar.gz", hash = "sha256:75a6a1a4ea954935dd3fb69b6e49ac0e3e3d69b4290127a0c8d37af93b709f3a"}, ] [package.extras] schema = ["jsonschema (>=4.21)", "pydantic (>=2)"] +[[package]] +name = "jsonref" +version = "1.1.0" +description = "jsonref is a library for automatic dereferencing of JSON Reference objects for Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9"}, + {file = "jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552"}, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -1520,6 +2927,26 @@ rpds-py = ">=0.25.0" format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] +[[package]] +name = "jsonschema-path" +version = "0.5.0" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = "<4.0.0,>=3.10" +files = [ + {file = "jsonschema_path-0.5.0-py3-none-any.whl", hash = "sha256:2790a070bc7abb08ea3dbe4d340ece4efadf639223001f020c7503229ba068e2"}, + {file = "jsonschema_path-0.5.0.tar.gz", hash = "sha256:493b156ba895c97602655b620a8456caa2ce08c1aa389f5a7addec065e6e855c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +pathable = ">=0.6.0,<0.7.0" +PyYAML = ">=5.1" +referencing = "<0.38.0" + +[package.extras] +requests = ["requests (>=2.31.0,<3.0.0)"] + [[package]] name = "jsonschema-specifications" version = "2025.9.1" @@ -1534,6 +2961,35 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + [[package]] name = "librt" version = "0.15.0" @@ -1682,19 +3138,57 @@ files = [ [[package]] name = "litellm" -version = "1.99.0" +version = "1.91.5" +description = "Library to easily interface with LLM API providers" +optional = false +python-versions = "<3.14,>=3.10" +files = [ + {file = "litellm-1.91.5-py3-none-any.whl", hash = "sha256:db09c6ea0b7c7de034b6c8b628208b000b54e02ef557468f84c2e63fbd7c8560"}, + {file = "litellm-1.91.5.tar.gz", hash = "sha256:f3a69ce82f61216903ca5e08aabe76a535862d4b935d7a7863953cbc8947e2f7"}, +] + +[package.dependencies] +aiohttp = ">=3.14.2,<4.0" +click = ">=8.0.0,<9.0" +fastuuid = ">=0.14.0,<1.0" +httpx = ">=0.28.0,<1.0" +importlib-metadata = ">=8.0.0,<9.0" +jinja2 = ">=3.1.6,<4.0" +jsonschema = ">=4.0.0,<5.0" +openai = ">=2.20.0,<3.0.0" +pydantic = ">=2.10.0,<3.0.0" +python-dotenv = ">=1.0.0,<2.0" +tiktoken = ">=0.8.0,<1.0" +tokenizers = ">=0.21.0,<1.0" + +[package.extras] +caching = ["diskcache (>=5.6.3,<6.0)"] +cli = ["pyyaml (>=6.0.3,<7.0)", "requests (>=2.32.0,<3.0)", "rich (>=13.9.4,<14.0)"] +extra-proxy = ["a2a-sdk (>=0.3.24,<1.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)", "litellm-enterprise (==0.1.44)", "litellm-proxy-extras (==0.4.74)", "mcp (>=1.26.0,<2.0)", "orjson (>=3.11.6,<4.0)", "polars (>=1.38.1,<2.0)", "pydantic-settings (>=2.14.1,<3.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)"] +semantic-router = ["aurelio-sdk (>=0.0.19,<1.0)", "semantic-router (>=0.1.15,<1.0)"] +stt-nvidia-riva = ["audioread (>=3.0.1)", "numpy (>=1.26.0)", "nvidia-riva-client (>=2.15.0)", "soundfile (>=0.12.1)"] +utils = ["numpydoc (>=1.8.0,<2.0)"] + +[[package]] +name = "litellm" +version = "1.102.1" description = "Library to easily interface with LLM API providers" optional = false python-versions = "<3.15,>=3.10" files = [ - {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"}, + {file = "litellm-1.102.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:01550e79e052249dc5df53631ed9bdff030ca7c2a22107c5ce482d53ecf5c8d8"}, + {file = "litellm-1.102.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:da73fe3f6a1bf70232d752f7e1fe49f5d4d9473c02972553e0d0f7b514b280c7"}, + {file = "litellm-1.102.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:296c49171d7b80e6298fdf1bb18c7b69a826b82ae2b917bc6f5a80f153ee8b29"}, + {file = "litellm-1.102.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b36c46e89e2fc50f42848120ed03a31c7347103cbaa733421f8843915b6f3355"}, + {file = "litellm-1.102.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7c4ffead69cf010fe3f551d2941bd6190bdaa816b98d5ebc85df67096724180"}, + {file = "litellm-1.102.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c5fcfbe322e3052f96fa92409fc61ec9712a7b2b567e7760e057b17b7587d021"}, + {file = "litellm-1.102.1-cp310-abi3-win_amd64.whl", hash = "sha256:b2c24cad24c637b20e91f6dd7b812971a5fa64ad2330213e517ff8f38d25c0d8"}, + {file = "litellm-1.102.1.tar.gz", hash = "sha256:7216f46e6a1eba50f4dbb6000afdb1f5b77360eb498a4f341fc655191108cfd4"}, ] [package.dependencies] @@ -1716,13 +3210,14 @@ 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)", "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)"] +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)", "tomlkit (>=0.13.3,<1.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)", "psycopg (>=3.2,<4.0)", "psycopg-binary (>=3.2,<4.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)"] +mcp = ["mcp (>=1.28.1,<2.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.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)"] +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.67)", "litellm-proxy-extras (==0.4.97)", "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.5,<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)", "tomlkit (>=0.13.3,<1.0)", "uvicorn (>=0.33.0,<1.0)", "uvloop (>=0.22.1,<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.16.1,<7.0)", "sentry-sdk (>=2.21.0,<3.0)"] saml = ["python3-saml (>=1.16.0,<2.0)"] semantic-router = ["aurelio-sdk (>=0.0.19,<1.0)", "semantic-router (>=0.1.15,<1.0)"] stt-nvidia-riva = ["audioread (>=3.0.1)", "numpy (>=1.26.0)", "nvidia-riva-client (>=2.15.0)", "soundfile (>=0.12.1)"] @@ -1730,187 +3225,187 @@ utils = ["numpydoc (>=1.8.0,<2.0)"] [[package]] name = "lxml" -version = "6.1.2" +version = "6.1.3" 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.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"}, + {file = "lxml-6.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:40bcbd9f94166ffe925811e730607385cec959f42fb1bb7dad83748680465221"}, + {file = "lxml-6.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:05f5bce9af14fd1506997594bd81cee6d9c6b58ea80a39c058327aa6371ed9e9"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff88a92cafde90888511242d1c54afcc1a8adbb6dc0a88fa7f87e29e92400d4a"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c00e26288784460885fe76e4d4b293573e0f791f52e6d60e27b42edf005922eb"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:773062aec2f2e56b2b22d37054123f0de8a22a4688a0c3376c3fe42685f975cf"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6449672f9c93316deb5e2839e18931f468670e44d5bd9b1301a5a9655d45c07"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:ec295280f4b37769256da025acf5890370355ac589c27e89caae0b5e9eedc702"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:5929d9df5e7e3379183be0e21f7d559618a5b61cb63280df6164019242e337ed"}, + {file = "lxml-6.1.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e1eb8a4cbffd5553680ad96be6680e364710656eced73d1dc90ec489df599a3"}, + {file = "lxml-6.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16148acd77ed1d8836a56db883af2f5eed720f9723088110b16a0d08582130a6"}, + {file = "lxml-6.1.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:23c366231259cd75ad06495174701afb3fcb36a92917fa47de2d1f1bd9d95739"}, + {file = "lxml-6.1.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:da85db328e507da922d586c3c7416ec360ec22e9cd9e0700691afacde0c81f53"}, + {file = "lxml-6.1.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f17d83c48ee9dfd96abae3ac3e2108c76d2fc86ce96355e37b8da9f7f4ecc08"}, + {file = "lxml-6.1.3-cp310-cp310-win32.whl", hash = "sha256:7dd624c1eaa629ad44b59a1a0145fdf2d67895592dce94c9358b938b3d075e65"}, + {file = "lxml-6.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:18a4db52b5a7b53a3540b0b0f4123319334621ee8083d496de314d0bf06ff59a"}, + {file = "lxml-6.1.3-cp310-cp310-win_arm64.whl", hash = "sha256:0feebef8d0521188d0157f758356072e840173aa61ca45b8b3f87959ac283dd5"}, + {file = "lxml-6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11"}, + {file = "lxml-6.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054"}, + {file = "lxml-6.1.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6"}, + {file = "lxml-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c"}, + {file = "lxml-6.1.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48"}, + {file = "lxml-6.1.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0"}, + {file = "lxml-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12"}, + {file = "lxml-6.1.3-cp311-cp311-win32.whl", hash = "sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633"}, + {file = "lxml-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559"}, + {file = "lxml-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6"}, + {file = "lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc"}, + {file = "lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f"}, + {file = "lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5"}, + {file = "lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385"}, + {file = "lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d"}, + {file = "lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9"}, + {file = "lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e"}, + {file = "lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5"}, + {file = "lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c"}, + {file = "lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c"}, + {file = "lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa"}, + {file = "lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd"}, + {file = "lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0"}, + {file = "lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e"}, + {file = "lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2"}, + {file = "lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310"}, + {file = "lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748"}, + {file = "lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d"}, + {file = "lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc"}, + {file = "lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87"}, + {file = "lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477"}, + {file = "lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1"}, + {file = "lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165"}, + {file = "lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023"}, + {file = "lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e"}, + {file = "lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92"}, + {file = "lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48"}, + {file = "lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d"}, + {file = "lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559"}, + {file = "lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415"}, + {file = "lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d"}, + {file = "lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861"}, + {file = "lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376"}, + {file = "lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f"}, + {file = "lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70"}, + {file = "lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7"}, + {file = "lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2"}, + {file = "lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c"}, + {file = "lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8"}, + {file = "lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb"}, + {file = "lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8"}, + {file = "lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a"}, + {file = "lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2"}, + {file = "lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026"}, + {file = "lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0"}, + {file = "lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9"}, + {file = "lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79"}, + {file = "lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015"}, + {file = "lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a"}, + {file = "lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed"}, + {file = "lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156"}, + {file = "lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d"}, + {file = "lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0"}, + {file = "lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69"}, + {file = "lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0"}, + {file = "lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4"}, + {file = "lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4"}, + {file = "lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad"}, + {file = "lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758"}, + {file = "lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe"}, + {file = "lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741"}, + {file = "lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300"}, + {file = "lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0"}, + {file = "lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd"}, + {file = "lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e"}, + {file = "lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2"}, + {file = "lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a"}, + {file = "lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011"}, + {file = "lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5"}, + {file = "lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a"}, + {file = "lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887"}, + {file = "lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e"}, + {file = "lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6"}, + {file = "lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf"}, + {file = "lxml-6.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:13a620a3fcc20023f9e6ed5c383e00e826f1c2d5db554df2f67240760f9118e8"}, + {file = "lxml-6.1.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fbfb70ba01355251faf6b293171df49f73a88a1b6494db109ffea85442574458"}, + {file = "lxml-6.1.3-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:302f72413251c03f671e063c9414bed5dc8c927069e5abb69245521e51a4e81b"}, + {file = "lxml-6.1.3-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:ce1f220114959941170e22b8ad44279f6dee2dcef7591814d01ae805dc058889"}, + {file = "lxml-6.1.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:170773d8a3cdc76259065523ddd978c44f9806e28605f08812e8f86783e44ac6"}, + {file = "lxml-6.1.3-cp38-cp38-win32.whl", hash = "sha256:92d96586376fb79a33474797186bf993250152ee5c32650b67db78d54b92e6f3"}, + {file = "lxml-6.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:d44442effeb8781f392340c5dc8c6716fba41dbeacb82fd4c0f09026fb5ff682"}, + {file = "lxml-6.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:869dfcd4d381cb0ea87085cc4f011b9171b494ef21e76ad8665f6d5e2d1dc8a1"}, + {file = "lxml-6.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6ba4fe5bfbef6811a8e49b3719cde373ad399006c0c1ac184b7297116ecbba5d"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:61116cec57ed69aebc70f37a545eec095339bb829efbdabcfb97c51e9536e158"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e11e885e0704be185867fcf71b904d8f65d7d6877bc121f69870b0d0479ba7b"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41e2d428110b408e963b6fb18f9bbf1f5c027b56bd4b498d54556476c0aeb1c3"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa9fd1ee2a5dacfc41039ed49ffeeacfa75bafbd255b69f3b578e11897a0e623"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:7f75b9b9fec2a9c6b18095c81865580e795b1441c429e42d22fcc82a77f40039"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:cc669256d28736f7f3a149df5c380c50ace2692ba3e62203d10656fade4a2145"}, + {file = "lxml-6.1.3-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d077f21f4b16f0471353883748f126f62038760397c107bb9fad2ca94dc0dfb7"}, + {file = "lxml-6.1.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0d12846d6ce434fb3857918eef4315ec9b4769deb020c75828798614bfcfd"}, + {file = "lxml-6.1.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:2b9b1325ca1c2a9a2dbb6eb913ae563313f2082ae60b03210f7e83ee80712274"}, + {file = "lxml-6.1.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a2e3f70673a1d5b82f38255f777d26cd855bf2092b1436c4867464a7892f9238"}, + {file = "lxml-6.1.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c34ca1dc41bd86d9ff830d5bdf4e4a752bba6c54f7d2707027ce0eabd36084c9"}, + {file = "lxml-6.1.3-cp39-cp39-win32.whl", hash = "sha256:b50343241eb69fd85f7791cf8bcc7b1c4729826b7d59ba2f6b27db29638fa745"}, + {file = "lxml-6.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:0794e04ba343852c6d78e996c58ef4b8e579b4ecc72f8df0d4058bf843b4c96e"}, + {file = "lxml-6.1.3-cp39-cp39-win_arm64.whl", hash = "sha256:0ab2467e405e748d93495fb5568e74044802b8d3ff2b2a1607c3f78c6e982de5"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b061064b4a2fe8598a466d723d43dbcd5a610a5d5cfe02fb6226f5c17349f75"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8499d464de86fab0f102313cce32a9bed9ab1f06ec813cf025cb790964fbb765"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e67324961ac9bbe616cce5100514d2e34d88665aeb07071e8b16eac55d06d94"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d12669a2c419b0e8dc423d23dea24bb82f6f9cb829f32e04674b0ba40322a7c"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97acecb11cbc411473f15b8d780df06d7a9f3a2aad9aca78364f56640c8fb70e"}, + {file = "lxml-6.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f8b9c8ceebae6387d0dc77f7f4dbbfbfc962dba2efbfe6877486075a480726b4"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f"}, + {file = "lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3847e71a78cbbc1aff955dbbbaf2fff12153f611d3162c5beaa3395636cbc2f9"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe91993149523aa59941b9e3c90e2eb45f57ad014697aef6c8b13339a59c019e"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71532ebf30be0048a45559b4fab15333fbaaf9042f658e878d918ecd0cf09805"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1b50797ac246bb2942a04b6c0f69af0667aba7cf7535f39bbb1b3208fd5d128"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2bb7d703bed7ac893bf7f40d97b5d9279d35d2ce460624ca28929eab0d5a3d"}, + {file = "lxml-6.1.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:be5346653c0b0e34be96869ff9dbeba23860156f89a2896a64c64fb419260cb6"}, + {file = "lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21"}, ] [package.extras] @@ -1919,6 +3414,66 @@ html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] +[[package]] +name = "magika" +version = "0.6.3" +description = "A tool to determine the content type of a file with deep learning" +optional = false +python-versions = ">=3.8" +files = [ + {file = "magika-0.6.3-py3-none-any.whl", hash = "sha256:eda443d08006ee495e02083b32e51b98cb3696ab595a7d13900d8e2ef506ec9d"}, + {file = "magika-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:86901e64b05dde5faff408c9b8245495b2e1fd4c226e3393d3d2a3fee65c504b"}, + {file = "magika-0.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d9661eedbdf445ac9567e97e7ceefb93545d77a6a32858139ea966b5806fb64"}, + {file = "magika-0.6.3-py3-none-win_amd64.whl", hash = "sha256:e57f75674447b20cab4db928ae58ab264d7d8582b55183a0b876711c2b2787f3"}, + {file = "magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1"}, +] + +[package.dependencies] +click = ">=8.1.7" +numpy = [ + {version = ">=1.24", markers = "python_version < \"3.12\""}, + {version = ">=1.26", markers = "python_version >= \"3.12\" and python_version < \"3.13\""}, + {version = ">=2.1.0", markers = "python_version >= \"3.13\""}, +] +onnxruntime = [ + {version = ">=1.17.0", markers = "python_version > \"3.9\""}, + {version = "<=1.20.1", markers = "sys_platform == \"win32\""}, +] +python-dotenv = ">=1.0.1" + +[[package]] +name = "mako" +version = "1.4.3" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.10" +files = [ + {file = "mako-1.4.3-py3-none-any.whl", hash = "sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f"}, + {file = "mako-1.4.3.tar.gz", hash = "sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua (>=4.16)"] +testing = ["pytest"] + +[[package]] +name = "mammoth" +version = "1.11.0" +description = "Convert Word documents from docx to simple and clean HTML and Markdown" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mammoth-1.11.0-py2.py3-none-any.whl", hash = "sha256:c077ab0d450bd7c0c6ecd529a23bf7e0fa8190c929e28998308ff4eada3f063b"}, + {file = "mammoth-1.11.0.tar.gz", hash = "sha256:a0f59e442f34d5b6447f4b0999306cbf3e67aaabfa8cb516f878fb1456744637"}, +] + +[package.dependencies] +cobble = ">=0.1.3,<0.2" + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1957,6 +3512,46 @@ files = [ beautifulsoup4 = ">=4.9,<5" six = ">=1.15,<2" +[[package]] +name = "markitdown" +version = "0.1.8" +description = "Utility tool for converting various files to Markdown" +optional = false +python-versions = "<3.15,>=3.10" +files = [ + {file = "markitdown-0.1.8-py3-none-any.whl", hash = "sha256:de7375a50578a39bcbbf13b48c67d99033d988e0ae8ad25af46ed432dbe4cbab"}, + {file = "markitdown-0.1.8.tar.gz", hash = "sha256:17188ad827ea79fc264c7b1ca8cf5a242a16278d84cc32f2edc475dbe92812ed"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +charset-normalizer = "*" +defusedxml = "*" +lxml = {version = "*", optional = true, markers = "extra == \"docx\""} +magika = ">=0.6.1,<0.7.0" +mammoth = {version = ">=1.11.0,<1.12.0", optional = true, markers = "extra == \"docx\""} +markdownify = "*" +openpyxl = {version = "*", optional = true, markers = "extra == \"xlsx\""} +pandas = {version = "*", optional = true, markers = "extra == \"xls\" or extra == \"xlsx\""} +pdfminer-six = {version = ">=20251230", optional = true, markers = "extra == \"pdf\""} +pdfplumber = {version = ">=0.11.9", optional = true, markers = "extra == \"pdf\""} +python-pptx = {version = "*", optional = true, markers = "extra == \"pptx\""} +requests = "*" +xlrd = {version = "*", optional = true, markers = "extra == \"xls\""} + +[package.extras] +all = ["azure-ai-contentunderstanding (>=1.2.0b1)", "azure-ai-documentintelligence", "azure-identity", "lxml", "mammoth (>=1.11.0,<1.12.0)", "olefile", "openpyxl", "pandas", "pdfminer-six (>=20251230)", "pdfplumber (>=0.11.9)", "pydub", "python-pptx", "speechrecognition", "xlrd", "youtube-transcript-api (>=1.2.3,<1.3.0)"] +audio-transcription = ["pydub", "speechrecognition"] +az-content-understanding = ["azure-ai-contentunderstanding (>=1.2.0b1)", "azure-identity"] +az-doc-intel = ["azure-ai-documentintelligence", "azure-identity"] +docx = ["lxml", "mammoth (>=1.11.0,<1.12.0)"] +outlook = ["olefile"] +pdf = ["pdfminer-six (>=20251230)", "pdfplumber (>=0.11.9)"] +pptx = ["python-pptx"] +xls = ["pandas", "xlrd"] +xlsx = ["openpyxl", "pandas"] +youtube-transcription = ["youtube-transcript-api (>=1.2.3,<1.3.0)"] + [[package]] name = "markupsafe" version = "3.0.3" @@ -2057,35 +3652,49 @@ files = [ [[package]] name = "mcp" -version = "1.29.1" +version = "2.2.0" description = "Model Context Protocol SDK" optional = false python-versions = ">=3.10" files = [ - {file = "mcp-1.29.1-py3-none-any.whl", hash = "sha256:b6310eeb59153300c4ab8b9aec4c52f4819a2d6a8e429eb43d908bed7c783648"}, - {file = "mcp-1.29.1.tar.gz", hash = "sha256:1967ba4c315f7a375146209949f45950d18b0efd2f913d7cf3400bc723ee5f04"}, + {file = "mcp-2.2.0-py3-none-any.whl", hash = "sha256:bde982589473a060ae145e3406e9a5333fe538c97229ba841f5a7f92be004f81"}, + {file = "mcp-2.2.0.tar.gz", hash = "sha256:2dc37ecb1974becdcebdbf7561e7c15a07dbbf20ba21ba16c3593b3038b3afbd"}, ] [package.dependencies] -anyio = ">=4.5" -httpx = ">=0.27.1,<1.0.0" -httpx-sse = ">=0.4" +anyio = {version = ">=4.9", markers = "python_version < \"3.14\""} +httpx2 = ">=2.5.0" jsonschema = ">=4.20.0" -pydantic = {version = ">=2.11.0,<3.0.0", markers = "python_version < \"3.14\""} -pydantic-settings = ">=2.5.2" +mcp-types = "2.2.0" +opentelemetry-api = ">=1.28.0" +pydantic = ">=2.12.0" pyjwt = {version = ">=2.10.1", extras = ["crypto"]} python-multipart = ">=0.0.9" -pywin32 = {version = ">=310", markers = "sys_platform == \"win32\" and python_version < \"3.14\""} -sse-starlette = ">=1.6.1" +pywin32 = {version = ">=311", markers = "sys_platform == \"win32\""} +sse-starlette = ">=3.0.0" starlette = {version = ">=0.27", markers = "python_version < \"3.14\""} -typing-extensions = ">=4.9.0" +typing-extensions = ">=4.13.0" typing-inspection = ">=0.4.1" uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""} [package.extras] cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] rich = ["rich (>=13.9.4)"] -ws = ["websockets (>=15.0.1)"] + +[[package]] +name = "mcp-types" +version = "2.2.0" +description = "Model Context Protocol wire types" +optional = false +python-versions = ">=3.10" +files = [ + {file = "mcp_types-2.2.0-py3-none-any.whl", hash = "sha256:ea476b73ee86709ab5abc9452385ed36cc05907e582355622e294595c9a04f13"}, + {file = "mcp_types-2.2.0.tar.gz", hash = "sha256:d3ed53703ddd10d9c6399f29d322bb66f3f67ab41348ac8556ba23e07fedefad"}, +] + +[package.dependencies] +pydantic = ">=2.12.0" +typing-extensions = ">=4.13.0" [[package]] name = "mdurl" @@ -2098,159 +3707,212 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "more-itertools" +version = "11.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.10" +files = [ + {file = "more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192"}, + {file = "more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d"}, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4)"] +tests = ["pytest (>=4.6)"] + [[package]] name = "multidict" -version = "6.7.1" +version = "6.9.1" description = "multidict implementation" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, - {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, - {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, - {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, - {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, - {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, - {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, - {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, - {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, - {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, - {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, - {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, - {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, - {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, - {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, - {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, - {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, - {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, - {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, - {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, - {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, - {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, - {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, - {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, - {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, - {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, - {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aef74e9beabbd6c4aafc091dabff86d046ccf013ce1e4396c0fbb01b4cad9de8"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ca5ebe6d454f1e5496cf052386a559f1080bf2de75bf327ebca0a6003b79f19"}, + {file = "multidict-6.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f6d0c4b70f783305e73f9944d8efe6be1022550f0974ba0ae9d8893c0350fa"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8050e75af7e4c6e2d5260b84eeedb618f3e452e66473432e085b5d1b81429299"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4736d350371337825cac1793c9f7c40701c32a03912548c2a7608e51679cbb96"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:02fe09dc197b8ae7e355371e51e5dce2f39060cd5a8badf94904527e23a3f188"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad4528cdce058b684f75fad1faf4a6a6c992fe2f08376370ca66e4ce5916a84a"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8885e3808aedbd6725b921fb67dacaae0678933561ddd47c2b01315198c70e2e"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b6e6840421c83ccb60398e333b44f910b0907bb409597685ab2eedd1e22eab"}, + {file = "multidict-6.9.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c9648ed33dc8179e4ec04bbc73bd7f0038e1e81217a69467f61f02a78bf07e88"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d9d6544790ba50438a9c1a903c3c4afb1ec8a7832db5518549275b40ef0dd4b1"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1c9f3c25df6c9d3bbae4f6fd3f514b1c5c740a2110f56ccf36069c85417e289c"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf22b43f35b7dbb9f71e8ee2041b5c00029cdfc28ede3a2f31cf8906f9a6c126"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed78ccad4c7b421804f5d524b7740946a7ef75d89dc0dac52e9d7c24c472410"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3b1aa25f01238886a6baed9e13b9a9240ed344346c79ae280ae65e8603b21d5"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bc94a68ea5e18f8e85dc6b522bcb53093f692c8eb62b4837ac047da73956cbe4"}, + {file = "multidict-6.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73533644e1f69ea1164d56cc6505f564c6c44072b646b66d67df2d31fed0348c"}, + {file = "multidict-6.9.1-cp310-cp310-win32.whl", hash = "sha256:66987aa68b0f7c2a1cc5f388ca962b8ed92b10f79de38d6d0d8c716154f519d9"}, + {file = "multidict-6.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32b78c1e52ebd8e247bb68300b90b233300d8816faa008ed0713bc539fb6af0"}, + {file = "multidict-6.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:ab64ace1a68682d191d9bedd9d4c939406ad86b1d9f628180410644249ad46c2"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:910d4260512660484c0dc1588a316fbb35a40c081c36fc51d1225351af17cfe4"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33fa55b990f81c2927e01399ace0d18926c69d69baa8cdaa819424132fb97987"}, + {file = "multidict-6.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:369b5aa01b241cd3fea6890bdbb11a1425d87bf1515831500d518f4223e9d72c"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:803f8b575a71b1b299d677c28db0653459c79b5308874efec813f17b7457c7f1"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8a65621b98984a62e59403009591b8a5a7736273aefe1cab64cfb85b365cc07"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:13849a1d4f54c3809ae721e9e83ab28f5ea602f33660cb84eb6ef261eac706c1"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd9a137a4a9becda3094f3831cd026380f75f6855e051eefe4c73ade524f1cc3"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ae9614c317c50836689ce2dfde07c05fe0b16378562e2221746c3913ede3c80"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f1e362c9352b50ed120f001046fdbb80810c9d56580f4c3fc13bbe30823387"}, + {file = "multidict-6.9.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dd655518f136febd96c05131a76a863e32fc2a1d7acd4e3c959e3ceb77d8345"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c5d675da8f1cb5650271c8ad5e95c0a3e5a183c105e72d953b12877b1c8d0fd"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:658f90f49cf5af2441cad0a2b801c3ef520471989a1ec55bcb25b255b2ca8d2f"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43124fe172ada86d03ac3c8dc8179091341f6724d5e5d5b160e1587e4cd3761b"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f0adc22a4eb31e545221d93fc73a0f6a8cc2379d0f4309f71d1d17ba938b82"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7fc59e9ba821b220944ccfe0f89c9dc4745f6d097569992356eb03869f21e953"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:87cc632c88ee5dc80e12681047839304d98ee5c9a708d686505767001c9b8b9a"}, + {file = "multidict-6.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b828cf64d62dc09ac183f03c1aeedd164ade96a2ce4934109452edf29de1dd13"}, + {file = "multidict-6.9.1-cp311-cp311-win32.whl", hash = "sha256:2c1aeb92eea59d824f004341b26d5e4b47a8a441cf9726769b9a90abf9d0e08f"}, + {file = "multidict-6.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:5f89dad732280e7a10b74d40b91364f88e13c3f2c08c2ef83a8cd42f7a61af2e"}, + {file = "multidict-6.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:5800368526647146978389dfaa46da3356291e9f0fff9a4ef12e8c2bef964a0d"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:29138fef49828542e859828107e42e50d0e587c513b7eb4b2d92bade2b0860fe"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e31815d41cefc365489e591d105d2baceb2f65aa75d29471fbdbda8651e006"}, + {file = "multidict-6.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ed30be8918e18c8bed0a2e8b70639ecf02feb61ed00ca2e41cfcb2a50fa3f42"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:637f4ae36264bd7b8d9a60193acddc1d735ad52e8ed53a19931ea6d921fea8e5"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35fc236507fb1b3138f0af5ecd5f94ed752d4d6d826248eae425f86204013eea"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e58952f04772f59f11c6e007471449809a30165188669bca8fdb19dde40a8f24"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d35a4f1c63f07fbb8c8f9946dea98b21eddf6c57421585f71d91864be3ba2a24"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5f8771aaaed7f80e84a4e471d2f29ab6721e4595075e54d03ae1ed951b2000a"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:976fd7689d69ec78d67d31d38d396d8adb562f7e8368279f76aed4aa451fa06d"}, + {file = "multidict-6.9.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95052e8777a86bae87c0bd0b5ab22d809e3d1d02bf69e3e66ddda5ba75a05805"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1a8adfcaf96f587ab138476eaddef95f29b8a2a8a9afbfea8d2fd62180995d02"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1a53de2772cfb74559df2eb4456ec4eeb908435ec55a84b69370d9d745d62aa8"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f2ce963299d42fa3f22a90adc0fdf174792ffef5ff4c7ffb68260548fb05580"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5b30ddf7234e611ca877575b62840e6af5977f92f1f9d532eedbb05a44ff8004"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63ada7ee2e9345695f9e9bc4c65d72222253f07b1ac94fd0e37555cc6f3c7f60"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c95601ed98fad3f6e2f8fe809c3b526b0fab31ef525e00a155e227f3d17f58a"}, + {file = "multidict-6.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c148e8b596000dd3e4bfe206e70f3e666be18d72032e0012555f2373c52e35d6"}, + {file = "multidict-6.9.1-cp312-cp312-win32.whl", hash = "sha256:f9dad513626a33670f17cddc6078e30e311f444c957e8dbfc5b2b4603c8b4edb"}, + {file = "multidict-6.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a16a1dc8529f9e734a41c3b856f3eae7ebacdc061dde3f8a844e0c7889c97203"}, + {file = "multidict-6.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:361f7206cf341ba94fb015688f5c8b480f8e63bd58a4c14a48aeca7851a241cc"}, + {file = "multidict-6.9.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:d7bf9e43282d69561618e8a0ea33368d532ebef42f15c096f427090521dd74f3"}, + {file = "multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:03d47df72f084f757c1cb771188d5f4e3a805e4abc4d67e32509272343ae9382"}, + {file = "multidict-6.9.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6bc94fe17c3c56e5418f79515b786b101845f70609b0d19d0c1ba13448e5633a"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8f2973bbd2bebd9d2e0cd6394c1292a1a19ccd56bdcbe1e174059f1a39be5b40"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de7738b8c0bb74c4cc16bbd7fb49fc2bcf6430dba11b3432cd52768ae40933e8"}, + {file = "multidict-6.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5ccad4b7bac125722f48d6f862bed3b514d8526deea06316bb72f152cd30a7c"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a49ff5cdb33654cb7d6a3c377aa2a83ddefaa1db31eb10bcf3c180aa84f9af8a"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b22ff30006a2f28f8bff878fb93413cbe3a4d1fd517c28081d848c90e9cfd2c8"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3adf06c66041aa21eeb8a71e82379b74773298c8e6d3d839b151aae441a99b94"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:963a8d8f97057082679523d0fd4c53a38f86bc58cabe4556faef682ae53fa2fa"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b66ccc5c2cdd26e74fa5d4c29ffae424cc6148bf93ce574821783fb3b6d452c5"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fd79c521f6290c69125fa2b85fa65d9e657e6a8ffaf722dc881b926bef4aa5c"}, + {file = "multidict-6.9.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d804e4caf5d5da37d6dac1325da5629ebef1e27a294c2b568b295814aa36c7b"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb0d664505f4b112f384cffeee82e91e3f6448d8e574989479db4439b68cba05"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9e14d17773b1b3c758ff153659a1824608a0cb562c45f484b5ed8a433428444a"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:083735b7f395894e43adb278d5dae901448883a835ff8f1977e285fefdb10418"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b65121091567847a8cb520d364ab22ba90e00d3cc55fa9eb34bb439f0684bcd1"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ea999ae6e80e66ad5eea287860951b033d0104ca34d6d87c7b5125ebe0e12721"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:095d900c242e00fbe5f321ee072e7278b4153e78c5ce9c1efde167d62c1e4771"}, + {file = "multidict-6.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6441cc837aea58be7d9baef1b2383eb8311ab9303f500f99ac90b584cd78bb14"}, + {file = "multidict-6.9.1-cp313-cp313-win32.whl", hash = "sha256:9c4880d017555d70dea367dd49271830842d48e3891c2da97da7ce8c4abcee40"}, + {file = "multidict-6.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:ac51cd64bae51c462ea58ad2492c9b8209667a4ef60c45c4a304518b67598d5d"}, + {file = "multidict-6.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:37a9ebe00c698279213d56e6c64e1962ab1e092918270649b397cac3dc196ca4"}, + {file = "multidict-6.9.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:fc0dcb22fa9aeabfe3fa4e0430099acff985ec5d77a851382f76cc6146e780e5"}, + {file = "multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:024123f0ab402ab33828e24eb80fa8f25167d0d3783ba5f287e39ed741e6abf9"}, + {file = "multidict-6.9.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:af1b5a92315048c3e36bbebfa7d4760a9c3e910bc4166f11b20d77d20d6bcfca"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2483da477932ad1983d1d33c18bc3771c6fb00cfbaaed70a875fd547ef8e840"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db4d697b18b6ef5528b1f36bfa25072cd2a421869f5963bc0e92c8a34b9e2800"}, + {file = "multidict-6.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:854fd2f1bc6e8a56b89910b5cd7261a8b40f13ebb31572da985ce59c7da0886d"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:03731f6fc036180700c9dc2308205a48e5ca6f3ff03087739ab746f294022201"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989261c5f1735a165f2e4e87cf6d5f17ab734fa18f9ad0383d5adfcdaefce701"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4c9e5d05b126b267ac048a89a0e2d9b48b1b648add62d4872906304a8590610"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d0a21cf76153de8f2d96a877991d6bc59b9ab5180b949e50db73cc193b4a694"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7386aa18d98d6b8af44b92173654ec469237fda35f8e8523e43b581a86f476a"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b69651732c64afb691e50cdc3387cae305e0eeff8804fe3e3ce203876494932a"}, + {file = "multidict-6.9.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1fee9a16d88a1c4865610de31ef5c666671020d7a81d1f510eaf3c97d00ebeba"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7814bbee202acd3bd240204c17d8b87a4c48c81064fd8674dbe527d94d5a4290"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:95f273bea318a194f656527ee2ed19494327bc500b8e87d9358e3222579aa28d"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:73bcafa21a78d0776b3ee7cd2a63c66f968eb7db8e8d594e32d7329950f6e828"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a938761c77e0e6edb0c93d02f4e988d44a69e5195e5a3b893e5553311347132"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:37245ca4105386194dd1d292a6f2aae09bfe1bd7ac6f9ec25093e3cf8e9b143b"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f0700527dd5bfa8b7204b08330542f4f388899d3c14d885d8a368992e0eb562d"}, + {file = "multidict-6.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b7e54fd883671d1a8810704851c044b7173287c552b9f5c3d9e0eb9f00ae194"}, + {file = "multidict-6.9.1-cp314-cp314-win32.whl", hash = "sha256:e81ae656b9935ac4528a71f96bb7a14d949778ed1897c573d3e7ebb9187f8841"}, + {file = "multidict-6.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:acddcac38adc8342ba48aba98896faa7928854bebb62542362138655b5367ee3"}, + {file = "multidict-6.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:32217133dddc58c927805cf6c0731d8144584176b768042ee886d51e71860bc9"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0b2fb8c349d1103863750b5d8cb5ace766917f4b35f3d883c8f778853eaa9f76"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:56d834b74c993a7d7cb2b8ab33a0d55c3e0d4a3d2f2da2808a4ad3d79189711b"}, + {file = "multidict-6.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:783ba7d845d79ce976afd9c1e91a4e5714671defa198ee789e8b23316083a485"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d718fa1b5f0d0dd75e86fbbc5b0c93ea3a5d65216c85c61cd5d7cdddfe08455"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a313bad717dde740959d50850c315b75fd4eb0c5e6b4dc8535db0f1c369be125"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ef01d29fca550ab871fd99154f82c6472aacf8e7def272dfb07b460123850390"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f3bd290711c6e9486173a6ee7cd4e7f00c3971c7908c1ec1b6e5437c5e4c6f9"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:81cdc537e0a42e3c0170752fcadbe450246d5c3b4b7231d6eb9672456605ac94"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b193bf7a443c97d81c47052f60c486071a4bdef4a573fa2514f920089414d45"}, + {file = "multidict-6.9.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a0c98f6a636adf0d7edd60c61589eaf52d149239af754d0bfeb0effedba53a8"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6e6b7e3a1520c39a2772f414cd9ddf5995f77e4e823dfa1522af383addd465a3"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ca65cced0d67a9039e93bcd98a369920e499bf98296844ff56ead01c9085321f"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:372c063f37480f62c1ae32dc3a1a0a5942b180c883f99789075a8a8ec3c4e709"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:827c92145b3b976b39430129c89d213b250dacbe5fce678e9a03940e6e848983"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9b0124b9c17e9890f0819b2e7a5f65ec9a2f5aabd6c8e7ad090c1675cf70dee6"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:23136f5a564654eb61061ec6d5620a4c1ea32c8f552b65e9982a12b72bff601b"}, + {file = "multidict-6.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f2524ec55b3e65cbe235a8b3c36e2af3b635be02ed05e20c94e70c5c943c009e"}, + {file = "multidict-6.9.1-cp314-cp314t-win32.whl", hash = "sha256:35ba0263bae5dd3ad5aad767cc9afc01a8598c7dae30f1b3b2de98b1b32c28bd"}, + {file = "multidict-6.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:7c8d5882ba25ac0282258be435d8a05aa0cbcacfce15799154a338f847f159b9"}, + {file = "multidict-6.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ac348379cf4de5538a0a213be1532d289aa801ec5d267c5909446b9ea2f8e2c3"}, + {file = "multidict-6.9.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:f04551dce5a7db8c9659f2e4245494c182d0663b83661803e08d46bfcae5eda1"}, + {file = "multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c0c88085affd35c33e124e36930c5ad96aff9294ce195eaa0fd9cec962b64a82"}, + {file = "multidict-6.9.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a8b75dd3d3638d9a19f23e84af4ffab3b8940422c0df2da2a77005ef5aa3d7ea"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:006c4478de0a1876f4834e14255776286f09b9846b505fe63f67f9d173a9487c"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2784090c30a586d5b45197bd9c32f87fb927216cde302f6cfd76d76577e90f08"}, + {file = "multidict-6.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c708566da8014b120a64b1eb6d200c6c0c8cb36296383723cdb6fc82038270b"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:042fb0196047e786936a730bd302de83143950da45f2c16078da8f35e1cf7919"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2212a0c842c723d919ea4a22a9296cb6b244b386e4e6ea92adfc7fbf3095519"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd882aa29bf402b62bf1fd7c19fd5df4b6528cf468a908864b39368572b662a9"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c57541034d12b215ab0a2bfa371d1a8a198da18176d0426c27105b9161a6862d"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d73fed4e37158ff00cd271871170b79e40138db51e625fd352fa17c6acb34f67"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79da2491348b30810728050b4a8ec0416f85884125c2fd44655b3d01150d9c3e"}, + {file = "multidict-6.9.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4da208a63434d21a3df64d29758e650fc4aa8cb05848554b76949c296539cca"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:877ca17fdcfdf5c397493a71e5ff97a87bb181417fe717fdadc77c08c09301ac"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:40aec5299e1ed71fbb988389059da381c3c1a60e0c649acceb2a35d9b128848e"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4a409ccc42aefec904038695d5d7fd6d8f3af2721b7b6401d55135ec7d6d298e"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0a5769559e3312dd96731fbe15b4abb6033368ac1cad5a98dadd21946a4c7d6c"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:03fac50ddfd8302175b77863a015eccfae76767cda5506eef86df559ba861e1f"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:5ecace251ccfa705bf3d7d35c5032cf750f5c629809740405697bce5c118c4a4"}, + {file = "multidict-6.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ecc89dbd4155b2f8a47f4bbd89242a35ed15e8e0ec581cab5ac65fa38407329d"}, + {file = "multidict-6.9.1-cp315-cp315-win32.whl", hash = "sha256:a3ffe881246d28a862f1985824f484cb7361f44d6b99c4c620436ef56462f38d"}, + {file = "multidict-6.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:59c123d0e948d760a5f930f316cfefa07e8d632ab84327c0693ee6a88171154f"}, + {file = "multidict-6.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:31199204b3ced121ff5407a2c342326a5d27e3870abbf94bd80dbd2451b7bc8f"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:8879510a76940670517ea1cb589978da44b86e286ec3e50d664ef330817afce7"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:aec65b53a07f580606593f877eefbb29a45939bfc0d3fe6e6d9f42b41b749f68"}, + {file = "multidict-6.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:14c56f73e78faa1f68bbb826197cd5871994e70e841b8590829c35912ece5c64"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:38c9986f9ce50c459b10de216a05f4bd7ed5ac63887d56e500a52bb464b861ce"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15db6a102cbaf1949cf028ecf080aac76d20bcd29ad4e092574db6c6b7af78a5"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd82c4977196681a499bb6ca9e462afbc5c91c1c15b6a991dfbd72733fea4dd2"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb58ba73a3f96f9a3e46b1fab69929d7edbbddb3133ee74b5c3c54074a53c4f1"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c6eecfab7ce4cd9487ff8ba936fe38cdfd68c04faf3d5c710363c6a8e695659"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c62e71e6289d78c0108d8aeb495f8bd3cad4bc1632fedddc9297ddf287ecc20"}, + {file = "multidict-6.9.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:539c2cd5fed0947c135cd7eabaaac55f48300dfa1de0f3ca4edb5efa6606f471"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3100d169ceb7bc8f05f89a6db11d1b21f119975fc26f29dc45a472e3569f0879"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:696477ad71385c4795e3b8e4cf10b0d2c28c2a1ca6a955e031cb1e62993e9ee3"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f5844e7befc707367807586f550fa97e23dcfef0728f02b98c7bc498a961a5df"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:abc7c2e4b47bfe6a9aea434d3fdebb9597ee636e914e92ba352f2068b9142f4c"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7ab379f95caee071a37cbd8be86d4c65accc651d391f9f97748fef006f38769c"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:557a4e1708df428ebe6c3081c83a273d275dad2446ce0d81fc648ac71afdb18d"}, + {file = "multidict-6.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:475d04d5192eba487a3e2f935976340baa24529046e9c1c9c7a3b7bf80445ae1"}, + {file = "multidict-6.9.1-cp315-cp315t-win32.whl", hash = "sha256:10083a8a0f4e1b26b599889e90b9802504ce5d3f7722f925bbb7ca47dd22a7c1"}, + {file = "multidict-6.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e96ca64383efa107262ee3949f047af5ee4f1845ba09463466c04d35a83bd3ec"}, + {file = "multidict-6.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:501ed8b02a5990c67a91c732843609d43a6be1f7576fcdfc867331239f37fbd3"}, + {file = "multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b"}, + {file = "multidict-6.9.1.tar.gz", hash = "sha256:0f06e60fa190aa7abd0914c2a766736fdc8e9f34878c4346338534b73d1b20e2"}, ] [[package]] @@ -2339,6 +4001,232 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "numpy" +version = "2.4.6" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +files = [ + {file = "numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8"}, + {file = "numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47"}, + {file = "numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8"}, + {file = "numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6"}, + {file = "numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8"}, + {file = "numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147"}, + {file = "numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41"}, + {file = "numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f"}, + {file = "numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a"}, + {file = "numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2"}, + {file = "numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45"}, + {file = "numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751"}, + {file = "numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f"}, + {file = "numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b"}, + {file = "numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a"}, + {file = "numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605"}, + {file = "numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91"}, + {file = "numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359"}, + {file = "numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe"}, + {file = "numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20"}, + {file = "numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67"}, + {file = "numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd"}, + {file = "numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab"}, + {file = "numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75"}, + {file = "numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5"}, + {file = "numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b"}, + {file = "numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402"}, + {file = "numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb"}, + {file = "numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1"}, + {file = "numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261"}, + {file = "numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e"}, + {file = "numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43"}, + {file = "numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895"}, + {file = "numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4"}, + {file = "numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063"}, + {file = "numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627"}, + {file = "numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02"}, + {file = "numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73"}, + {file = "numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda"}, +] + +[[package]] +name = "obstore" +version = "0.11.1" +description = "" +optional = false +python-versions = ">=3.10" +files = [ + {file = "obstore-0.11.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8cc4681d45196645b0567cf7259730fcb0edba751ecad8c4c3a6ccc63fabe24"}, + {file = "obstore-0.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3e657c600b90f465ef43d00e6dd921e676a1bec34713dad1fc09ef6715e30658"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614a36f19963cfefedd52f06c5370c034a6dc5b0a2be2e4602fe6ac8e52e97a6"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:066cb4190e957b172b269affb98bee5a8f5cf6d0949b5b57ffe7f8cfaa540779"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdc2d273b7656d862c7295d2dab45987a2c22197d60f1cad14c8b86c5af0b452"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c71e619d9965375241566beda8358031fdce8cb136310d4f5b71faa88f278ad1"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32dcdae4207656ef353f949f927342e31757f926c58f014873b2d90c70276cb7"}, + {file = "obstore-0.11.1-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:446dfe0019740da25394917585b8b0c116ae79454a633ee83aeb0c973c8812d2"}, + {file = "obstore-0.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1dd9d7ddc10bbf17cea0e84553a665a822878bd199380557a6df2aa1e60ce34"}, + {file = "obstore-0.11.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:75a12f1d0d38fd2923e972e417797d7bf123ff7d13e2b6c5919d2e9a3c6653cc"}, + {file = "obstore-0.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ebfb22202530863aaf00ebb9352fd17cdd1066aae50986006d76b10c1e8506e9"}, + {file = "obstore-0.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63be447e5fdaf0c31517a66906030148697ab3b1f1611fe03e30125a56199ccc"}, + {file = "obstore-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eab3231ce66bc08e57b686b795d4a1333133c28b2e50c11f9986311dec69899"}, + {file = "obstore-0.11.1-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d5c50b755d781efebe4f9c70ee6d00858e44d95f0229b8b5174358f861d9bd7c"}, + {file = "obstore-0.11.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:093152daa5c32b70a032f231bbc6a7eff76dda4285eed5a81d272b4485032dab"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b97ee10456e65f166c030b5fbde8380ca508621e23b84efd77aad83afe99315a"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48983a143de69b11de49212caa79b5c39acbff7f592f9e85d156a0133a0e5796"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b9cb988e03ff963914cf2176aee293eaaff7dcf4efcb905ce6834264b4b0884"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97850f68c8417f7167549bdb705c362c825ed470298c6b04faf52258c5e9234e"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9f3d66dbf3c073dd6b6033c0787ef14edf78d97bff95595d6f6979692da264"}, + {file = "obstore-0.11.1-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:2ba3bceec4263b3a70eea873abedb82e03da9599f2326e80d6505cab0c10d401"}, + {file = "obstore-0.11.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e956fa8a953b7eb8580658d68cdf37e410356032c17697a06e44d0e8c4084a0f"}, + {file = "obstore-0.11.1-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:159a50d0f4cc53afe6f5c695313bcaf6e92ac81d7847692c8279153483272bfa"}, + {file = "obstore-0.11.1-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:128da07f3a1b9c70159e2b2be9e27f458a9d531d57b1856dd7c4ddb73b4c9937"}, + {file = "obstore-0.11.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:67b8acfbbab960c2cb14c34294a96556caa67fcff15ee77c716d5c1bd1558606"}, + {file = "obstore-0.11.1-cp311-abi3-win_amd64.whl", hash = "sha256:e23ea15cebe5f5be5d11005043d7b5ff56848e39499681ef52f8227bd385bd51"}, + {file = "obstore-0.11.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:73284fc8a9804596baf4d80b55c0e71a6007487bfa28d6924acd85264d5be81f"}, + {file = "obstore-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8a3f93310422b153629af929d9e88d1fc826d5e32a456de4d3a1926a770ae09c"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d666690f0a53ae3df4be2c18af6820369a74c9c5b8960e8760f0f8c9a8f15b36"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0844ab75c8413c0af2d0fd2d2f6aa42d166809b6ac4be478151cd946ce7e5d69"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:307b5f9d64a7c00cd13371e376c05e3914216bd0818a19ff2ae05bc0135e01dc"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f89647953b4ea50bab3f66591f7f462cd454a5d2fbbeefd885716a2c54e13ce"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50de3116af69b6f1669cb443cc200ccdc1523a790a41f00854331b48f716cf8f"}, + {file = "obstore-0.11.1-cp314-cp314t-manylinux_2_24_aarch64.whl", hash = "sha256:eae71e1c5944ade976ce8cd1e780bed9b5c31d6d4f89cb7263e8dcff78f6cd8e"}, + {file = "obstore-0.11.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff634b5edbbf76c56ae81397aa41500a66ad0aa48b1856dcc0cf31b159172dc0"}, + {file = "obstore-0.11.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cbe509350d66249fc9e65ece4e7b1855d650f18e477721887128452b95c44b24"}, + {file = "obstore-0.11.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:46122e585f48ab3f2e4fed51401a4e866279a3a3d1aee2372d598ab85ba2c539"}, + {file = "obstore-0.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7275e75228059b2b30772b2ba3e41562a4a92ebdd3a96619d300f98ef152119f"}, + {file = "obstore-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:13bb0b6a40931ab2da93f93ad843865d483d95acec1bd586df79313daa5a50af"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:68adc24a822536148a3c12be0b7cac091239b3f99c0589b16bcdba5a6ccf58e4"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ed098bea084f8d626d91facfebdc8fe49115b96a339465db6fea681635fc7440"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d80248f106bf9a2860a4b21add7d14c95eaa5785c7fe11e56d28a4602c7a07c"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ad616bd597a453dcb225ef6bec2af0c71b24b22c314a4328bcc216f774d6b9a"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a399e7c816e8d7bfe5e992a04e25326bd0779822b65bfd3dbc943b8a62192dc"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6680094928da4587dfd03be4e9c17ce7eaa1ec287f49519c75896819c0767247"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ab26d158a096c750759981f75f279f6568d945b8fe640e0cebfdff8d601044"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:7b1b769fda200cbee559da2100586b3b7810bc9f207fe41b9a00fd749c997799"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:801f57c06df5d81eaff8ed553129cedadad12cf1d76fb7499fd6daa1d996c72b"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:b895230ad67a7b9a2c7dfff7c7c9801570a9dfe71f7e383013f831d35b74ea2b"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:955f2348bd17beb80f3f96bb52b119e61eee99c97d1f0b103aa61a0840b7c862"}, + {file = "obstore-0.11.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3901f539d764cd2ec74c4e900dd461d89d765b2cacf06379ac7b7014e8d132ca"}, + {file = "obstore-0.11.1.tar.gz", hash = "sha256:a5afe8b99e3b20cdc9133be7a1b381259acf0d470029f6b2fc79c3f9947ad436"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_full_version < \"3.13\""} + +[[package]] +name = "onnxruntime" +version = "1.20.1" +description = "ONNX Runtime is a runtime accelerator for Machine Learning models" +optional = false +python-versions = "*" +files = [ + {file = "onnxruntime-1.20.1-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:e50ba5ff7fed4f7d9253a6baf801ca2883cc08491f9d32d78a80da57256a5439"}, + {file = "onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b2908b50101a19e99c4d4e97ebb9905561daf61829403061c1adc1b588bc0de"}, + {file = "onnxruntime-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d82daaec24045a2e87598b8ac2b417b1cce623244e80e663882e9fe1aae86410"}, + {file = "onnxruntime-1.20.1-cp310-cp310-win32.whl", hash = "sha256:4c4b251a725a3b8cf2aab284f7d940c26094ecd9d442f07dd81ab5470e99b83f"}, + {file = "onnxruntime-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:d3b616bb53a77a9463707bb313637223380fc327f5064c9a782e8ec69c22e6a2"}, + {file = "onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b"}, + {file = "onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7"}, + {file = "onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc"}, + {file = "onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41"}, + {file = "onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221"}, + {file = "onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9"}, + {file = "onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172"}, + {file = "onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e"}, + {file = "onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120"}, + {file = "onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb"}, + {file = "onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc"}, + {file = "onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be"}, + {file = "onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3"}, + {file = "onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16"}, + {file = "onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8"}, + {file = "onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b"}, +] + +[package.dependencies] +coloredlogs = "*" +flatbuffers = "*" +numpy = ">=1.21.6" +packaging = "*" +protobuf = "*" +sympy = "*" + +[[package]] +name = "onnxruntime" +version = "1.30.0" +description = "ONNX Runtime is a runtime accelerator for Machine Learning models" +optional = false +python-versions = ">=3.11" +files = [ + {file = "onnxruntime-1.30.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f36c87ec504f4f328e968ee322e4ba9682abe66280b3425625afcbed5333fed7"}, + {file = "onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bac3aee7b40acd805659fd81a2acd6096312d5b0f014c97ddfe75fb4c1644627"}, + {file = "onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd54b314ea385bcecac69ab431f020ba503e3878dad4ebb645fec5a24b041242"}, + {file = "onnxruntime-1.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:0edd0145a6e3fce8a1276491dc784d615e3c58bcb952c9b4e5c876d5c6a12ad7"}, + {file = "onnxruntime-1.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:16506cfa5d218850f2b46e0edce1b952121f2f84f8e0074dafe15f4787f0c8fb"}, + {file = "onnxruntime-1.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd"}, + {file = "onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83"}, + {file = "onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328"}, + {file = "onnxruntime-1.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38"}, + {file = "onnxruntime-1.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262"}, + {file = "onnxruntime-1.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134"}, + {file = "onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9"}, + {file = "onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f"}, + {file = "onnxruntime-1.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93"}, + {file = "onnxruntime-1.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f"}, + {file = "onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e"}, + {file = "onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca"}, + {file = "onnxruntime-1.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547"}, + {file = "onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175"}, + {file = "onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec"}, + {file = "onnxruntime-1.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4"}, + {file = "onnxruntime-1.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d"}, + {file = "onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294"}, + {file = "onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7"}, +] + +[package.dependencies] +flatbuffers = "*" +numpy = ">=1.21.6" +packaging = "*" +protobuf = ">=4.25.8" + +[package.extras] +quantization = ["ml_dtypes"] +symbolic = ["sympy"] + [[package]] name = "openai" version = "2.54.0" @@ -2368,6 +4256,257 @@ httpx2 = ["anyio (>=4.10.0,<5)", "httpx (>=0.25.1,<1)", "httpx2 (>=2.7.0,<3)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +description = "Pydantic OpenAPI schema implementation" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146"}, + {file = "openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d"}, +] + +[package.dependencies] +pydantic = ">=1.8" + +[[package]] +name = "openpyxl" +version = "3.1.5" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[package.dependencies] +et-xmlfile = "*" + +[[package]] +name = "opentelemetry-api" +version = "1.45.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3"}, + {file = "opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7"}, +] + +[package.dependencies] +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-exporter-http-transport" +version = "0.66b0" +description = "OpenTelemetry Exporters HTTP transport" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_http_transport-0.66b0-py3-none-any.whl", hash = "sha256:c82689928a11505a2a0a26a04afe0ce06d270d3db984bc16c1a80ffa902db00e"}, + {file = "opentelemetry_exporter_http_transport-0.66b0.tar.gz", hash = "sha256:2c229b6593eaa22c86d9b8a15843dc23b406dbda00fb138339189aab07923b4e"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.15,<2.0" +urllib3 = {version = ">=1.26", optional = true, markers = "extra == \"urllib3\""} + +[package.extras] +requests = ["requests (>=2.25,<3.0)"] +urllib3 = ["urllib3 (>=1.26)"] + +[[package]] +name = "opentelemetry-exporter-otlp-common" +version = "0.66b0" +description = "OpenTelemetry OTLP HTTP export utilities" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_otlp_common-0.66b0-py3-none-any.whl", hash = "sha256:35d24c867310f4713a9738b0202b6bade77955418fccb9e2f7eea01b76263916"}, + {file = "opentelemetry_exporter_otlp_common-0.66b0.tar.gz", hash = "sha256:362268ec6aa705e183776ff938539df1e8ce45bc5509d242538b1d40c26fe6a6"}, +] + +[package.dependencies] +opentelemetry-sdk = ">=1.45.0,<1.46.0" + +[package.extras] +http = ["opentelemetry-exporter-http-transport (==0.66b0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.45.0" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.45.0-py3-none-any.whl", hash = "sha256:7e1410ae6d3ed301f7a74bec96467d519d3f37e52f9d95b309ae125e5a1af863"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.45.0.tar.gz", hash = "sha256:36495115a0c6a7aa946cfda9d59b6ed4e917b6ab0f75cdaf66bc1b176ec1be1f"}, +] + +[package.dependencies] +opentelemetry-proto = "1.45.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.45.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.45.0-py3-none-any.whl", hash = "sha256:74385f99266dafdefff41f3da4b76f6dba79d237a9d4487e6ff1ee653c74a0a3"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.45.0.tar.gz", hash = "sha256:2f35496d96809f946f41b8805e6b93aec6c9b71b5fd759b75b6af4c084d992ae"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-http-transport = {version = "0.66b0", extras = ["urllib3"]} +opentelemetry-exporter-otlp-common = "0.66b0" +opentelemetry-exporter-otlp-proto-common = "1.45.0" +opentelemetry-proto = "1.45.0" +opentelemetry-sdk = ">=1.45.0,<1.46.0" +typing-extensions = ">=4.5.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] +requests = ["opentelemetry-exporter-http-transport[requests] (==0.66b0)", "requests (>=2.7,<3.0)"] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.66b0" +description = "Prometheus Metric Exporter for OpenTelemetry" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_exporter_prometheus-0.66b0-py3-none-any.whl", hash = "sha256:12448682ee2824fef00bd290568297bb5bebd5fd8403fd7d70b3e567cf8b2a93"}, + {file = "opentelemetry_exporter_prometheus-0.66b0.tar.gz", hash = "sha256:941434dd88e7c0fb8c1ec0d4e5c1b62c6206bb47b8b35f287cbac56fea82bd40"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-sdk = ">=1.45.0,<1.46.0" +prometheus-client = ">=0.5.0,<1.0.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.66b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_instrumentation-0.66b0-py3-none-any.whl", hash = "sha256:0b32e2fcfec1e4bd48511a3b77ce32bfb1c16dcd5479396b21f9086a4b06c0c9"}, + {file = "opentelemetry_instrumentation-0.66b0.tar.gz", hash = "sha256:316715ed7b33a12530c0ae3e8d4cdf26d70e1aad358d7ba256e5dea2d4ceed71"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.66b0" +packaging = ">=18.0" +wrapt = ">=1.0.0,<3.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.66b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.66b0-py3-none-any.whl", hash = "sha256:8c31fe67b2aef6f7dc3df9c0434ebd0cacd6d8545da703a55a6f160f9bc06fc5"}, + {file = "opentelemetry_instrumentation_asgi-0.66b0.tar.gz", hash = "sha256:ab3f9c86aaddbf9c332534e0fce6c918cbacdab7a33951825a9ebba6b2fb7200"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.66b0" +opentelemetry-semantic-conventions = "0.66b0" +opentelemetry-util-http = "0.66b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.66b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.66b0-py3-none-any.whl", hash = "sha256:ad9543fc5a87b511d487aa94625a97299ccd818611cb0c979b46985ba782f1cc"}, + {file = "opentelemetry_instrumentation_fastapi-0.66b0.tar.gz", hash = "sha256:3d49e678d0957a782d0ca32ad21139cdfabe8584d82c018dc9faf6bfc5a72701"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.66b0" +opentelemetry-instrumentation-asgi = "0.66b0" +opentelemetry-semantic-conventions = "0.66b0" +opentelemetry-util-http = "0.66b0" + +[package.extras] +instruments = ["fastapi (>=0.92,<1.0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.45.0" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_proto-1.45.0-py3-none-any.whl", hash = "sha256:9731566359d7b8e1ee1e149d8e4ad6865bab965e657ad3387ec2784d16927666"}, + {file = "opentelemetry_proto-1.45.0.tar.gz", hash = "sha256:96ee414f24bc3f61ea8e17dc56b4348d4049d73db3eb17c6b3edf75b5b403300"}, +] + +[package.dependencies] +protobuf = ">=5.0,<8.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.45.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_sdk-1.45.0-py3-none-any.whl", hash = "sha256:5dc634c946546f61b757c5b1781f9fd1a10e96ffaf7357c797e508fe9c57e75e"}, + {file = "opentelemetry_sdk-1.45.0.tar.gz", hash = "sha256:20caa5130505e386c67c3da1c76e446c842698ced54c76c6148679539aa97972"}, +] + +[package.dependencies] +opentelemetry-api = "1.45.0" +opentelemetry-semantic-conventions = "0.66b0" +typing-extensions = ">=4.5.0" + +[package.extras] +file-configuration = ["opentelemetry-configuration (==0.66b0)"] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.66b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_semantic_conventions-0.66b0-py3-none-any.whl", hash = "sha256:175b19dd98c4473f4f43a2b1df59186fd7b4a48cd3f77cbe03438b6d6fda230a"}, + {file = "opentelemetry_semantic_conventions-0.66b0.tar.gz", hash = "sha256:97a77dce484c54861e7eeff7651fd8a806dd3c30e501dc316730215ec36890e6"}, +] + +[package.dependencies] +opentelemetry-api = "1.45.0" +typing-extensions = ">=4.5.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.66b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.10" +files = [ + {file = "opentelemetry_util_http-0.66b0-py3-none-any.whl", hash = "sha256:7f5e581e94f023b30995f700e41ce319cec82bd542ff1c1c7c9b56d6b10bb687"}, + {file = "opentelemetry_util_http-0.66b0.tar.gz", hash = "sha256:a2d53859630abe8e5abcc4f867c5a809bb95873033573b1a67f6ef43b57a8627"}, +] + [[package]] name = "orjson" version = "3.12.0" @@ -2453,6 +4592,115 @@ files = [ {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] +[[package]] +name = "pandas" +version = "3.0.6" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.11" +files = [ + {file = "pandas-3.0.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:085e3786ae6b2e82b406266bce36690f72b9dc1421903ba9296b2981a9fcf586"}, + {file = "pandas-3.0.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7564d86a94c2eb8ab290b07f63ddaae5c032fa53897c29a2ff2197d43aee8af"}, + {file = "pandas-3.0.6-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7c0afdcaf6661d795fcefc2f647ddd1136f62cdc153fba177c685d97a87808"}, + {file = "pandas-3.0.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47121f9571503f724c9b93e297ab6254ac99c77adf5e9ed085ea419fd585c258"}, + {file = "pandas-3.0.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:994a79608263fe1c14cc48ffa7300e2b834b7d1cb406ffe96a08828cb0cdd79b"}, + {file = "pandas-3.0.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3a22e07fe75347eaacc75b0e85297947af4fba6b4aae23916bd8b6828d0bba3"}, + {file = "pandas-3.0.6-cp311-cp311-win_amd64.whl", hash = "sha256:2e5fa32ff162dfdbc280157d664f44d23049ae414725af9676df339c501d82cd"}, + {file = "pandas-3.0.6-cp311-cp311-win_arm64.whl", hash = "sha256:5e75072773c1b2f7cb63faa3a6f562aede11f3976f68ed34cb538bc091a28171"}, + {file = "pandas-3.0.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dac2d65e9087e8e7b5a45fe15c4920911a221df061ab629943ce016489145c7"}, + {file = "pandas-3.0.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dab635a549e58a053c7b0fa054dc0bd7be22f0ed9a720f4a85d5fb993276172"}, + {file = "pandas-3.0.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3dccb584123b399c07562ac4d62543e90ede49ddf8ce3c13ffc64cbe828c281"}, + {file = "pandas-3.0.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0704044b676496b8350e023b09f174a26772456c974a2b11c36bebb558c9490d"}, + {file = "pandas-3.0.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e7c1905ef02c3d6d43d9dbd5b6ccb4da4870a0b0c821bbc103fbdb6f3ad2707b"}, + {file = "pandas-3.0.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:569e114072b24fc4970c12e2b4bab252671668a40b324318903380cab0254c0c"}, + {file = "pandas-3.0.6-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:2a8fc94be2ee5f1d86f97aacd8cc566f81680b6498e76f3007421bb5d98151bf"}, + {file = "pandas-3.0.6-cp312-cp312-win_amd64.whl", hash = "sha256:3ef908d28590b3f42d7070e7ad8f9b34b442b260b7f3c1afb57e0040c58cdb1b"}, + {file = "pandas-3.0.6-cp312-cp312-win_arm64.whl", hash = "sha256:f4e7c52eb108d752e7592268108fd3e98efd76d83a3125cdd06c621c2e44359b"}, + {file = "pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2"}, + {file = "pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa"}, + {file = "pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c"}, + {file = "pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658"}, + {file = "pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2"}, + {file = "pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d"}, + {file = "pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd"}, + {file = "pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f"}, + {file = "pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1"}, + {file = "pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729"}, + {file = "pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34"}, + {file = "pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1"}, + {file = "pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de"}, + {file = "pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c"}, + {file = "pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553"}, + {file = "pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c"}, + {file = "pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514"}, + {file = "pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60"}, + {file = "pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541"}, + {file = "pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965"}, + {file = "pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7"}, + {file = "pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44"}, + {file = "pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630"}, + {file = "pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a"}, + {file = "pandas-3.0.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570"}, + {file = "pandas-3.0.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34"}, + {file = "pandas-3.0.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e"}, + {file = "pandas-3.0.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c"}, + {file = "pandas-3.0.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19"}, + {file = "pandas-3.0.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e"}, + {file = "pandas-3.0.6-cp315-cp315-win_amd64.whl", hash = "sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de"}, + {file = "pandas-3.0.6-cp315-cp315-win_arm64.whl", hash = "sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7"}, + {file = "pandas-3.0.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640"}, + {file = "pandas-3.0.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36"}, + {file = "pandas-3.0.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804"}, + {file = "pandas-3.0.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e"}, + {file = "pandas-3.0.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266"}, + {file = "pandas-3.0.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947"}, + {file = "pandas-3.0.6-cp315-cp315t-win_amd64.whl", hash = "sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a"}, + {file = "pandas-3.0.6-cp315-cp315t-win_arm64.whl", hash = "sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0"}, + {file = "pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10"}, +] + +[package.dependencies] +numpy = {version = ">=1.26.0", markers = "python_version < \"3.14\""} +python-dateutil = ">=2.8.2" +tzdata = {version = "*", markers = "sys_platform == \"win32\" or sys_platform == \"emscripten\""} + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)", "beautifulsoup4 (>=4.12.3)", "bottleneck (>=1.4.2)", "fastparquet (>=2024.11.0)", "fsspec (>=2024.10.0)", "gcsfs (>=2024.10.0)", "html5lib (>=1.1)", "hypothesis (>=6.116.0)", "jinja2 (>=3.1.5)", "lxml (>=5.3.0)", "matplotlib (>=3.9.3)", "numba (>=0.60.0)", "numexpr (>=2.10.2)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "psycopg2 (>=2.9.10)", "pyarrow (>=13.0.0)", "pyiceberg (>=0.8.1)", "pymysql (>=1.1.1)", "pyreadstat (>=1.2.8)", "pytest (>=8.3.4)", "pytest-xdist (>=3.6.1)", "python-calamine (>=0.3.0)", "pytz (>=2020.1)", "pyxlsb (>=1.0.10)", "qtpy (>=2.4.2)", "s3fs (>=2024.10.0)", "scipy (>=1.14.1)", "tables (>=3.10.1)", "tabulate (>=0.9.0)", "xarray (>=2024.10.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)", "zstandard (>=0.23.0)"] +aws = ["s3fs (>=2024.10.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.4.2)"] +compression = ["zstandard (>=0.23.0)"] +computation = ["scipy (>=1.14.1)", "xarray (>=2024.10.0)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.5)", "python-calamine (>=0.3.0)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.2.0)"] +feather = ["pyarrow (>=13.0.0)"] +fss = ["fsspec (>=2024.10.0)"] +gcp = ["gcsfs (>=2024.10.0)"] +hdf5 = ["tables (>=3.10.1)"] +html = ["beautifulsoup4 (>=4.12.3)", "html5lib (>=1.1)", "lxml (>=5.3.0)"] +iceberg = ["pyiceberg (>=0.8.1)"] +mysql = ["SQLAlchemy (>=2.0.36)", "pymysql (>=1.1.1)"] +output-formatting = ["jinja2 (>=3.1.5)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=13.0.0)"] +performance = ["bottleneck (>=1.4.2)", "numba (>=0.60.0)", "numexpr (>=2.10.2)"] +plot = ["matplotlib (>=3.9.3)"] +postgresql = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "psycopg2 (>=2.9.10)"] +pyarrow = ["pyarrow (>=13.0.0)"] +spss = ["pyreadstat (>=1.2.8)"] +sql-other = ["SQLAlchemy (>=2.0.36)", "adbc-driver-postgresql (>=1.2.0)", "adbc-driver-sqlite (>=1.2.0)"] +test = ["hypothesis (>=6.116.0)", "pytest (>=8.3.4,<9.1)", "pytest-xdist (>=3.6.1)"] +timezone = ["pytz (>=2020.1)"] +xml = ["lxml (>=5.3.0)"] + +[[package]] +name = "pathable" +version = "0.6.0" +description = "Object-oriented paths" +optional = false +python-versions = "<4.0,>=3.10" +files = [ + {file = "pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566"}, + {file = "pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58"}, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -2469,24 +4717,184 @@ hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] +[[package]] +name = "pdfminer-six" +version = "20260107" +description = "PDF parser and analyzer" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9"}, + {file = "pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602"}, +] + +[package.dependencies] +charset-normalizer = ">=2.0.0" +cryptography = ">=36.0.0" + +[package.extras] +image = ["Pillow"] + +[[package]] +name = "pdfplumber" +version = "0.11.10" +description = "Plumb a PDF for detailed information about each char, rectangle, and line." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pdfplumber-0.11.10-py3-none-any.whl", hash = "sha256:7741ea81bf165b474b153e6789d10d18e06b6ddcf3ec84289c3ef2fed6802580"}, + {file = "pdfplumber-0.11.10.tar.gz", hash = "sha256:b95b2d28c66efb0a794a83b88c6c6aea5987532a445d20a1cbcfa657022e6e57"}, +] + +[package.dependencies] +"pdfminer.six" = "20260107" +Pillow = ">=12.2.0" +pypdfium2 = ">=5.9.0" + [[package]] name = "pg0-embedded" -version = "0.15.1" +version = "0.15.2" 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"}, + {file = "pg0_embedded-0.15.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:cbfb988ef6ad39009835f7d1f734b6df5bc05db944050f932e08465bcc6bfe2d"}, + {file = "pg0_embedded-0.15.2-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:27a4a6bba5bbe536fd948eab121686946ddce5fa7b2a5ecfdbb70ee9db0169bc"}, + {file = "pg0_embedded-0.15.2-py3-none-manylinux_2_35_aarch64.whl", hash = "sha256:aaf89e3c4cfe8affb87910f13dd1e8604596f5f3da7efff5a5eb812bd8b4437c"}, + {file = "pg0_embedded-0.15.2-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:c55f43e36de626d4d1cd1ea2f8897e17779c3b5a38cba0107329a149dddfae18"}, + {file = "pg0_embedded-0.15.2-py3-none-win_amd64.whl", hash = "sha256:1b47c938991243d938e28c7e1998c1298637fdb15c54ada6d853e02769d50d3e"}, + {file = "pg0_embedded-0.15.2.tar.gz", hash = "sha256:f7181c10aa4db55d61fdc85eca927fb0bd0cb49b1fd5e9898f1606df53757fd4"}, ] [package.extras] dev = ["pytest (>=8.0.0)"] +[[package]] +name = "pgvector" +version = "0.5.0" +description = "pgvector support for Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f"}, + {file = "pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74"}, +] + +[[package]] +name = "pillow" +version = "12.3.0" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed"}, + {file = "pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1"}, + {file = "pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb"}, + {file = "pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5"}, + {file = "pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b"}, + {file = "pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a"}, + {file = "pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df"}, + {file = "pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f"}, + {file = "pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09"}, + {file = "pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e"}, + {file = "pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f"}, + {file = "pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8"}, + {file = "pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130"}, + {file = "pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a"}, + {file = "pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d"}, + {file = "pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931"}, + {file = "pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7"}, + {file = "pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c"}, + {file = "pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71"}, + {file = "pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827"}, + {file = "pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5"}, + {file = "pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9"}, + {file = "pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8"}, + {file = "pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418"}, + {file = "pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a"}, + {file = "pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "setuptools", "trove-classifiers (>=2024.10.12)"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.11.15" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +files = [ + {file = "platformdirs-4.11.15-py3-none-any.whl", hash = "sha256:84508421c77d3c6462e71dd8c0d2776c2ace0e94eb006dc4fd383bc1dcf5a176"}, + {file = "platformdirs-4.11.15.tar.gz", hash = "sha256:d7419e973b2b740d428200130f80c0e79304d1c71081001db911e15b26a3a6d4"}, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -2504,282 +4912,489 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "primp" -version = "2.0.0" +version = "2.0.1" description = "HTTP client that can impersonate web browsers" optional = false -python-versions = ">=3.10" +python-versions = ">=3.10" +files = [ + {file = "primp-2.0.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4296ae43a8660bcb1dfc1570ed07dc9f8ab64f11fdebf3272532411b7fe321ef"}, + {file = "primp-2.0.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca21c764f17ba42dde38c29d6a1f01970d39fa5f4793b0fe702006bd71b7a16a"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa85a55b1c53ef8c2d14f1c61f0b7ab0ff300b349b2768a52d6c2f3f3f9ca80c"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9edc6d2f2fd30d2ddf8c093bf533a3e99ae1385a1d2af15baaa299fb55310fb8"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24d3ffeb054c23c7588d0240b5488d960f669398d1ab4cb3873158322bdf821d"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0beec080cb61044bd8b2eb0e3ca60f15f2df8ced9f2fbc6f1760855c20aed42a"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7be373adfded677a9092ae2743873d5c8a9573148d617a189d26715f7d8ea5"}, + {file = "primp-2.0.1-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:acc8b31f7fcc241b8ecb94069efea611f4f451d90f1798684c379d05e108b2c0"}, + {file = "primp-2.0.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5f8d170c69b4afbe61d854b3f0cf27a0c0e557f0e54ebe595dad4db0f72127d"}, + {file = "primp-2.0.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:706c843c86162d431c5a2051b8b41e16c7c51bca6bd60d139d7614609463a6c5"}, + {file = "primp-2.0.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:415aa6bb1b998ace5a456734df95755b5342b73fa0a6babbcb3ca471c83b9dbe"}, + {file = "primp-2.0.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7f494686da2991212f5607d125c32cb8da72a92e179d497c7d2ffeba21abff1e"}, + {file = "primp-2.0.1-cp310-abi3-win32.whl", hash = "sha256:1cb429afd3a5ea98c625292f3c6581b0ccc0d398d3307603cce25ec140dfd671"}, + {file = "primp-2.0.1-cp310-abi3-win_amd64.whl", hash = "sha256:0e27f3e233cf34cae6cbc8158af58b93fb0a87ceb1802b4611c7a14a2d822cdc"}, + {file = "primp-2.0.1-cp310-abi3-win_arm64.whl", hash = "sha256:dea9370fcf6624725f564f9b9cf4fec3c127f86b7494196d03343a835fe3dee4"}, + {file = "primp-2.0.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:656ea4ff0d45bbd119394a6834a367e34192d75cb7a5945cfc2f1cbb266b1be4"}, + {file = "primp-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:31b3cd957f02dc49e5e9d9cbadd809747872caebfaf093dc34775a7f866a3e65"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b8af35eb64d61291b105479245c89ed1231a5fff1e9b75870515892ffabf054"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f659e6479073c4693195f0f43b7a96ae0afd353f6f057754f80be016eea6f20"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba278a9af63981f2aece0d98fec9d6cc5a918943eb56e4f4df2b3ca90dab787"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bedb42ea9188dd571db46d93f0b994d2e8e9271d2ddd55ee0ab7ac2844742bed"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:888ba0708e518acad84bfc0a8bb274ec7ae48655a95afdb9c8184bd88cc6d7ab"}, + {file = "primp-2.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:71fa07394c0084940d86c9f441bc2f05d7f8951a944bde315bebb6eec9b718d7"}, + {file = "primp-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c8f2b2eaacddf2bdff7b1b5f4219249d06246e577432fcb9b9babbc65146ff32"}, + {file = "primp-2.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:934696c74b8a88a7dbb40b3bb54a182b0c9782427f036035cb21bfc0cf7e24a9"}, + {file = "primp-2.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5f2e7b9fe557a440a929746a318074fd9989be318ce75411d01f1f3ed7bc85d"}, + {file = "primp-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b4970ab274deaa13224777fb52b8745523293c23566a6c44fa3fbd61e47c183"}, + {file = "primp-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:0440d84854d1f9218277eef2c688a1774408e0d8a3805077eb6db432a4fa7970"}, + {file = "primp-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f0806c7653ee05bbe3c7b28f97f19bd5d763d7da1366026d5cb85cd8713f5c33"}, + {file = "primp-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a26651747b21efdff1ff986ee3e98ec3b349ce84b01b22226ce0b7a967041f01"}, + {file = "primp-2.0.1.tar.gz", hash = "sha256:82ba17b077bef19a189d9ec8d77ca632496cb444e0f4fa37e27e90041cf0da8f"}, +] + +[package.extras] +dev = ["certifi", "mypy (>=1.14.1)", "pytest (>=8.1.1)", "pytest-asyncio (>=0.25.3)", "ruff (>=0.9.2)", "typing-extensions"] + +[[package]] +name = "prometheus-client" +version = "0.26.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.9" files = [ - {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"}, + {file = "prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6"}, + {file = "prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b"}, ] [package.extras] -dev = ["certifi", "mypy (>=1.14.1)", "pytest (>=8.1.1)", "pytest-asyncio (>=0.25.3)", "ruff (>=0.9.2)", "typing-extensions"] +aiohttp = ["aiohttp"] +django = ["django"] +twisted = ["twisted"] [[package]] name = "propcache" -version = "0.5.2" +version = "0.5.4" description = "Accelerated property cache" optional = false python-versions = ">=3.10" files = [ - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, - {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, - {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, - {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, - {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, - {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, - {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, - {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, - {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, - {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, - {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, - {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, - {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, - {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, - {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, - {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, - {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, - {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, - {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, - {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, - {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, - {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, - {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, - {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e"}, + {file = "propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2"}, + {file = "propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb"}, + {file = "propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f"}, + {file = "propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8"}, + {file = "propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745"}, + {file = "propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d"}, + {file = "propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5"}, + {file = "propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d"}, + {file = "propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd"}, + {file = "propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9"}, + {file = "propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858"}, + {file = "propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8"}, + {file = "propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b"}, + {file = "propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031"}, + {file = "propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161"}, + {file = "propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8"}, + {file = "propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb"}, + {file = "propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0"}, + {file = "propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572"}, + {file = "propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1"}, + {file = "propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c"}, + {file = "propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25"}, + {file = "propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77"}, + {file = "propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993"}, + {file = "propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9"}, + {file = "propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792"}, + {file = "propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b"}, + {file = "propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea"}, + {file = "propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983"}, + {file = "propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5"}, + {file = "propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370"}, + {file = "propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e"}, + {file = "propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57"}, + {file = "propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97"}, + {file = "propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12"}, + {file = "propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51"}, + {file = "propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c"}, + {file = "propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c"}, + {file = "propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38"}, + {file = "propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123"}, + {file = "propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3"}, + {file = "propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3"}, + {file = "propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72"}, + {file = "propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284"}, + {file = "propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad"}, + {file = "propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec"}, + {file = "propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432"}, + {file = "propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e"}, + {file = "propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468"}, + {file = "propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558"}, +] + +[[package]] +name = "protobuf" +version = "7.36.2" +description = "" +optional = false +python-versions = ">=3.10" +files = [ + {file = "protobuf-7.36.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:cbc70b17ee27e28894c7fee8bb04be1abead49e936bc70eb60052531eee2079e"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:e11e1f0180583a2af89db6a2ecd9e8dc40aa6d2988ca175bfd0e6d12ea72d74e"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:f4fee11ec330d238b34a05c9b675f693c20415d1c5bd7d5320cc2f8a798eb9cf"}, + {file = "protobuf-7.36.2-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:89f23aa53c24553a2416fd4fd1ec06f74fa42b14b546d8883128813f775bbfd2"}, + {file = "protobuf-7.36.2-cp310-abi3-win32.whl", hash = "sha256:912c1221170e16c08d1f086762f563dd61ff83c18b5fa6652952dfaded66f728"}, + {file = "protobuf-7.36.2-cp310-abi3-win_amd64.whl", hash = "sha256:a300819d441e078a5608c0d3c709796bb548136058fda017ae51d425b44fd353"}, + {file = "protobuf-7.36.2-py3-none-any.whl", hash = "sha256:bdb3a345d48db958e6ce1f18e508beb0cc981d64f24088427549c866cd039f1e"}, + {file = "protobuf-7.36.2.tar.gz", hash = "sha256:497d0463ff3316681da6c0b9e8d06cb465d61abce00b613ab42226175644d1bb"}, ] [[package]] name = "psycopg" -version = "3.3.5" +version = "3.3.6" 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"}, + {file = "psycopg-3.3.6-py3-none-any.whl", hash = "sha256:a1db9f7148b06a28606767efaca51fa6f9398c5c0a3810519be69d7000bdb631"}, + {file = "psycopg-3.3.6.tar.gz", hash = "sha256:c081f2250df751a943036e42db6df4571c66cd0aabe8291a7a506512b12007d2"}, ] [package.dependencies] -psycopg-binary = {version = "3.3.5", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-binary = {version = "3.3.6", 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)"] +binary = ["psycopg-binary (==3.3.6)"] +c = ["psycopg-c (==3.3.6)"] +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 (>=0.0.3)", "isort[colors] (>=6.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" +version = "3.3.6" 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"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7beb3e41c9a1e509f3ed85263386588cbe3e975aa67be21f79f44fd35ffaeefc"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa73160077345ec21b3f51e8e24b3de2e99586217e497629326eb9b2ea88c52e"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f87dbdc42e78ee0f7ea180c03f8c78e80a949e373066629bd90fefff10552dff"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a9348c5b43a3bb5ef8c2e89d5237c9c87eeafb01d338c84a7aebbc5cd0313299"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a52991594ac4db888c7d39bccef331797e30cb31a95cae02cf2607f83a42dc2"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ea8beeb5541780b4b50b462eeacbc4f594ce3b911dc20c81c75f267876f71d2"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:198a48e68cc99ccac03ba95ac857e73aa66f3bf6be77019fafb0832a05f7ad03"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fa34eb47969297471db7b7f193622c7e3ee839ec05abd05f1fe104d5b1b1dcf4"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b979a42815410432420275412633960807178b1ce26591a16ce06e78a5bd4bb2"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:889e42acec10450185e0cdfb396f375e2c1a8d7737c114830a7fde4654f59e30"}, + {file = "psycopg_binary-3.3.6-cp310-cp310-win_amd64.whl", hash = "sha256:cbd5f73073ed19c378d4c35499db1e3e703a5b1a324e521204065967bfaa7a18"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:be4f9b3c9338ac5dd217c5847e21521b396c8117f78dc420d495a5c49bbef874"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0535693ce476a722b718b002d5d2c27d47e71ca945276ac194409c98e74c492"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3c9e663b2e800e3218994cf948c11bcc2844e6491b34aa80d089baf6531827bf"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a2e44a342d2aee40508e28a563d8961c39d9bbd8cae36d8578f0a3c6658aab0f"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f598f19fa9a91540b5cee17932ffd227b7b53a481605bcc4573c0eafa647300"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff05561e4a067d35507dc5c90f1deb2ec1c9703ac5cccc1bc26e08a197f9c5a"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:566dd827f17728efdf7d88a5b066f815170f6fdad13967ae952842d90e6aaa9f"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9b2f11794e017ce340934e35de46181c46ef71ec75ea3d85dd75cd836761c01e"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:910ace140e3e7b7596898d083f37a8fe90c5c40684252ad4e682364b2cd3deba"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37e517c146b185f9c0c6e8d0a0ebbdeeeb67896af28466e032bc810d0c7dc7a7"}, + {file = "psycopg_binary-3.3.6-cp311-cp311-win_amd64.whl", hash = "sha256:c7f92daa0d2a1c76f07264abddf8cbabd30152a2f09c3270e50f0c7efdf5dcac"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f84dab25e0385692ee13274c68678377e0b1a70ab9d14e56264cbf61f60c62d"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:612382ac3ed13651c7fa44b5fee9fbf7baaa2ddbc6f500391672682c5f1df9e0"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:366db6e97e66b37211475f20c4c1324a2dc0dd825e46d4e87f9d599304d276f9"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1679a1cb93fbe5a6d1fd58d82cbddcc6fcb8c61446ba7cae6eb2a7b19bc585de"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d40450659401600e6d043ff586c89a71a69f33cbb8bcdba6cdb2569beecdbe"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5165300324efd5a772c48a88ab3a928513ab3979fca76553e62ee815f7b2b9c"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d636338c8f21b0df2f84657b00bc34f9313f826ef93f1155bc743607e4a0c5eb"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a4ee3bdd5468a725f2a4d9aab8a74b6d0279f768c8b5d3aeb102c5307ff3d59c"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:289aadd6a00e151203c081f708348ec89f1e483c9b510ef4ac3981f847f01f79"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f21d057f3e5f5491067e5b292498073b73847d48799b099803fef100775fcc52"}, + {file = "psycopg_binary-3.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:e23a66a763fbe83fcc210bc77c27e5a5ea380ebf091c06f34d8561b695e5a40f"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8f35e67cc16d1fad1fa8c88972dc9b3a3141ea67897399904edab96a301b6"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373704aea331d3f3e3402c125a1543f5875e2986ebb54f97d1647942161f803f"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b82491019b884d62318b5f30706c3d7e6d4e5a6cb7eabcb3edc0c1b0fdaceae9"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cec5ea900390897d0b46130f60bc2883bf19c314f9044235217c8be88b0ef269"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98c02090d88f2ebc0ec1e8da538f77d225ce0fffecf372aa39262e62a1b054ef"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ee2c4728c691245e24501fcd7a97b5b381236b9985bc445bba88cdce7d1b5784"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f19cc87343eaa55255e76b31259a570072ac95d6ae82c92dd34b97691f5e49dc"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdccb3a0e184b03e9baa673b15a809cf36c339c85dbda0ebc25a698846dfbee8"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9892188bb15e5803beb51afe8a25add6b56be391a53058e8bca03b74e1e6bf22"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3af90f92769d8cc10f94515ee7a0aef36ea85ca733a0ce22858f6e0953f41138"}, + {file = "psycopg_binary-3.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:0ebfad5d131de9f892ae9e70cc7616207768b6714b66a52d4612b8ceaf78b372"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b3f75dee0f9afafabe4edc52c4842f1e1878ed2069bd05b22d6fe961e97e4dba"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5927b7ba63153cd8e9862987290a2b783a5c590daf2a4ef981700cc3569166d4"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0bf08b749cc144f33b44a91b78e3f71c60eb07963746a0df5a100b36ce3d7475"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31cd942c23f613276b81a6e6598cefa12960058b0f46e1e874b540c793f6aca5"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4690cf67738f0e0e49a32aeec99bf0e4595cc2b4f1af984a4345394b1dcff91a"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad1c785e784cfd87e8436c6b7702f2d321fc39601bbaf29bc63a41a867091638"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:79a2a1c3449f6c3409427078ed1cec10de79f3023cb5f2504f0597d350ad46c7"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:86147cb5d140341c3363fb5bacce31f8d5543902a46699d3c536b101bbceaf9e"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7308c93cf0b19bbaf8e6ff0a6ad50d3c442385739245fe15a8d593bf841734a6"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05a83ac9fd52b9bca7cb5ab04b3691163170bd16f53defa27216ea3aa07ee781"}, + {file = "psycopg_binary-3.3.6-cp314-cp314-win_amd64.whl", hash = "sha256:1fbd30e537dab22cafdf080608f10148fe2a5f3a61294ddb5113caac8a623840"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bf8c8481d026b85dd70c5fa7dde85b2333aed0b32a2602bcd38a900cbd78a49c"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b599defe9190b17e9907c8b4d114c181e702c87efcd1b8a0ad40971cdcc4634a"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b8ece331509f7a975b90501f41e83ad905e4141753fedf3f2711b2bc70a8efbc"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c61617eaae0112ca154da87ffb99b73af2c74067acac28dfb9a4455b019dff2e"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6d19cb4999d03231e8730a5f66c8f5068bc3b532677eb39dab0f600bff3e312"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8cbb54454dbf1bbf2ff08dd7693e8d94ac94b1a20f70f4b3b813d52ecb5cbc1"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:dc75da5a20951049f7b773145f998f69d181adad9c58a0ff36e0cf1d73c10e10"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:955e3dd94da361e052d2e49acf591017158dc8f8ed2c8a42c2e3943403c39dc2"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:c7753871eb57e6a5f4646f6168590c6653073dea5e9e720b201c8875332df4c8"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:303732e798fe6729f8e12021b9c96107df8e95ecec4dd487c67b98ec2a59435e"}, + {file = "psycopg_binary-3.3.6-cp315-cp315-win_amd64.whl", hash = "sha256:2f122603f36050937982abf9668d8bc4769a79f7c93a65013b1c49f1cab7b56b"}, ] [[package]] name = "psycopg-pool" -version = "3.3.1" +version = "3.3.3" 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"}, + {file = "psycopg_pool-3.3.3-py3-none-any.whl", hash = "sha256:9b9cd6a4fcec47a410f7e82d408540e7f77b478509e91b44c1a5457a13e5ff37"}, + {file = "psycopg_pool-3.3.3.tar.gz", hash = "sha256:df87b5d9d0ad7db37f6cdad4fa8ce113d250f5997f6db38e9a99192fb67f9e1d"}, ] [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)"] +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 = "psycopg2-binary" +version = "2.9.13" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.10" +files = [ + {file = "psycopg2_binary-2.9.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c519e406287085f43aa0d3061936edf1ba51286093532f215315c6ab8ba92c3b"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:086659ab083119f7ee87a779e31b94211cf162b708fc9a6bec771f75c73ac3e6"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1f4c7bdbafdf9dc018efbc29213b73f8308332888ba76a4cf503f560bfd21705"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d2fc9342aad969b9a28490a4c3eaba94b35beb2d26e9a39b31d1430378aa71b2"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f124954a32640dfb5c000d33028f48053930d7ff226bc74cde5fb316f9c6fcb6"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c24c98fe1a113db287dfb1958771eafca97b7db812f23b7897c2a12b6b904c22"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4cdfe41149dcc5583a3b7a2f0ad433f75bb3afd1c7a7332e63df89b05e34666"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33a6d3c47f9655b481b2cdc1b4bf71c235e054e55663d3066036b6ce5fbe5165"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:202dedd5cadb3e5dfd4d0415ab2fc5d5b44f4208de5308938e3e74ae222b638e"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db31cf7f617a51625f1473d8a66fc35dac159af8b28e80bc014ed3ee994a9fbf"}, + {file = "psycopg2_binary-2.9.13-cp310-cp310-win_amd64.whl", hash = "sha256:28eb30bf4a52c1117406f45771038faa96f882fdeeeb0ce43b960a1dbc6c1fd2"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d19aec88857d2a52f99eefcefdbbb45921fb2f777bee5186a355a23d9cf8a0b9"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32cd049095135d2b69e824aea9056745a4aaaa9115a9febbc65584793665d0d0"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e696297891b56ff0115f0665de6ad774e1e301e4f60745b8d5024001ae7c2f6"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:930e7e58b33a4f9c39e7532d7a40147925cf3372baed4229cbebe0cf3ba9ce6b"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aea95340825f5ff236e7b40f0b5602c2c77a1e95943f71fae34909834043d29"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27e539b4cafd5e03dcd32921db1b12dd72fe549dd06bae6d4d2a5b5838465f24"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6444ac48e2c04f691c2ddd542b38ba30c89463a2d446b3d74ec7d8fc90c964"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8cb734989420c18ca1b71a82da880e11988f5ff3fcdaadd669161de3e98794ac"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f47f23db2d70db39cfb714b64fd5df76595b51b2ec0a669710a78f2dceb0c3f8"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f28b5f2fa8154d0d97e97a664136f58d1639ca008d45d6e09e69fff24826abee"}, + {file = "psycopg2_binary-2.9.13-cp311-cp311-win_amd64.whl", hash = "sha256:70d091f5c3a6177fac50c0da20181ce0e0c053f1e43c872d5f75bd6d9429c020"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bf9f97a6df69a5d89d054b8cf5257a0916096c479800715fbfe7974dbcb3a26"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07b7bd9f410650c34c3532162cc329f112368d78a3fc8668cb1ea9df61bc11bf"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0463c00f946517f3e69192a59e6601e023ff9de45ad0a875eda3d6b1bebeb7ce"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3861eba31f8ea8663fd876166b032fd89179e42aa63764d6feb281f13f9eb60"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dc3372b3731b3ef23407fe06b94f640ef87a2bda242fa386033d5589c87514a"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0405dd4d97720e7ab177aa02e493f524907c4cb3c445ac173e2627948d3d0528"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6ae51708201f501a171b02419d0c30878a743c369c9054eb1289f0f8d5979e2"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81682c227cc1849c4a6adf7b85274229073bb4c9d6ad5697222c695dcea5a8a7"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:13d955f6054a705a19554364fe9888d0a6e8b0746dc7ebc08a447c7b4fd4145c"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e2405196a8cfe6cd3e54172a54452dcf85c241eaf2e9dde7190d7469f7f5ef7"}, + {file = "psycopg2_binary-2.9.13-cp312-cp312-win_amd64.whl", hash = "sha256:376ebf7d8aee4b7386b2bac31fdc27911e7e57cd0a88f1e038b8b149398ac008"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d66bfd44a46eb88cff0287929a4193fb45166b6c1f84bb1b233cc17ece0813c"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f818161d2302b3b3e9c75d5a1d0a5c5679e92e45cfec6432b9d5432dde5ff1f1"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:31db6cba66df5231dfd91d9f69188bec3fe6c8baae384e93a0ce792067ee2d98"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f04ada42bcd537adbaf8b7f3140237a204e452a88d0c1831cfce69f7d2e59f4e"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa37089795bd9701576edc2eb5849ce77a439eda9dfdfa47857449332cfa5292"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:41c2eb569ebd0e1b02d30d361a46932923b193fe1b5e641fb4d547c75e218955"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f699a5225094a5c61402984e2fc1eca20e940223e76767c88189efb0c313f69"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5f04ae99c9fbb94c3197ec88599ed7db921f6adcddfe83687a74c7ead4037c22"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:81404c37e0344ebcf10aac127d33d35137e5dbab1daf9f3deee46188fd5879c2"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:feb7b1856f6ca805cc0e08739858f6cdfed8ce903390126af30343c62899a389"}, + {file = "psycopg2_binary-2.9.13-cp313-cp313-win_amd64.whl", hash = "sha256:691da68ae5dd7c3ac77514357d35ece7b1ba8b5f3e6c92735198aa6159c355c8"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ca263643ae37998ae04d18e431df34d0d61f12b47640dab585f14b6dbe00798"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c0214c7da18a28d108aa7108c8a3cca8035c7911ec97ef9ec0827569c9a2720"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d89e064bb12b40cad696cf4975e6da86f8c60f14cd06cb6c1bc0a7f5d01761f"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:190c18b97d9ef72f2e88c451b6588af90d6bd7bf54cb94b963280dc86a2c7076"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c00ebe9a2f31151aade0db233dc1446513a95e92c39ce055ee097af0ae86be1c"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5085f7ff7b1e890f279577cedeb8c628957869a340fa34a39f7f406500b3c916"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e55357d1943673d491bbabb171c891704fc6a22441fea539e05a5c27a79ea3c"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3e60b06ec7f9dc3e5f1106d12706514b6d6b92c3dc438fcdf4e43e65cc660d1b"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dde942b46ce20f6c4464cdf551f3293207f803f4e4354454eb1f5599c3eb1fa1"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:215777c62ce81c3b487cefdb6a41969944eb982309f91349ff3ca0323d6f17ed"}, + {file = "psycopg2_binary-2.9.13-cp314-cp314-win_amd64.whl", hash = "sha256:f3088eb80f58ed933c62d87128741d31e786edc862e23266d3c286763d646de0"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:38397def2d794ffde9db80f63d6820253e61b17483112652a318355f51a56f50"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:dff5c70ed9789ccb0d97ff4a7da51dc523a255c4ec95df188fa5d44adcae4ea8"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:08d3b81a6a91775c937abf97d4c58fc9142e8e35fb91c387d24f81d15c98e6cf"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:541a487a9ccd72b5e38f37f27b0ce78cb7eb3e336e7b5277d45463010c03a7a8"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:562fe2a43b30e781848dce63d9080c15414c777c96df348c4342558338cc7bf3"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dddfe650e7dda464d676c27fbedb5061f1ad05e1604627f54c770d7f799d36e9"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4ff0f575cbb14f30445858dcfdd751e043486f5290915df78a9818bc74042eff"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d79530b4c1af657d5620a1d21b8e39f2996aa06821d5564d05b22d6b8cd413d0"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:6ede8595767e19d30a7e8a84a7d47bfde6176d45d194fed08dbb68d1584a780b"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0ebcf3c4266a695df9d0ef51296155f60c86ac51cf82f0d0dd2e827255a891c5"}, + {file = "psycopg2_binary-2.9.13-cp315-cp315-win_amd64.whl", hash = "sha256:1752b9821f1377404d65ac43af03d59a1eccc57fb2c1eb8305f9a3fe8eb7a8ba"}, + {file = "psycopg2_binary-2.9.13.tar.gz", hash = "sha256:e324ecf60f952d21dd11413b8bbed0951bbd99579a06fd06f28bfc37737cd373"}, +] + +[[package]] +name = "py-key-value-aio" +version = "0.4.6" +description = "Async Key-Value Store - A pluggable interface for KV Stores" +optional = false +python-versions = ">=3.10" +files = [ + {file = "py_key_value_aio-0.4.6-py3-none-any.whl", hash = "sha256:820b30c7959fc738ea3d00160cdb10cd258f51e38737b2add530defeea1ded77"}, + {file = "py_key_value_aio-0.4.6.tar.gz", hash = "sha256:267c03c3e24cb99d3097612f8a5cfd8e11785c6a2975e272db0e303ea1850bfd"}, +] + +[package.dependencies] +aiofile = {version = ">=3.12.3", optional = true, markers = "python_full_version >= \"3.11\" and extra == \"filetree\""} +anyio = {version = ">=4.4.0", optional = true, markers = "extra == \"filetree\""} +beartype = ">=0.20.0" +cachetools = {version = ">=5.0.0", optional = true, markers = "extra == \"memory\""} +keyring = {version = ">=25.6.0", optional = true, markers = "extra == \"keyring\""} +typing-extensions = ">=4.15.0" + +[package.extras] +aerospike = ["aerospike (>=16.0.0)"] +azure-tables = ["azure-core (>=1.29.0)", "azure-data-tables (>=12.4.0)"] +chdb = ["chdb (>=4.1.1)"] +disk = ["diskcache (>=5.0.0)", "pathvalidate (>=3.3.1)"] +docs = ["mkdocs (>=1.6.0)", "mkdocs-material (>=9.5.0)", "mkdocstrings-python (>=1.10.0)", "mkdocstrings[python] (>=0.30.0)"] +duckdb = ["duckdb (>=1.1.1)", "pytz (>=2025.2)"] +dynamodb = ["aioboto3 (>=13.3.0)", "types-aiobotocore-dynamodb (>=2.16.0)"] +elasticsearch = ["aiohttp (>=3.12)", "elasticsearch (>=8.0.0)"] +filetree = ["aiofile (>=3.12.3)", "aiofile (>=3.5.0,!=3.9.0,<3.10)", "anyio (>=4.4.0)"] +firestore = ["google-auth (>=2.24.0)", "google-cloud-firestore (>=2.13.0)"] +keyring = ["keyring (>=25.6.0)"] +keyring-linux = ["dbus-python (>=1.4.0)", "keyring (>=25.6.0)"] +memcached = ["aiomcache (>=0.8.0)"] +memory = ["cachetools (>=5.0.0)"] +mongodb = ["pymongo (>=4.9.0)"] +opensearch = ["opensearch-py[async] (>=2.0.0)"] +postgresql = ["asyncpg (>=0.30.0)"] +pydantic = ["pydantic (>=2.11.9)"] +redis = ["redis (>=4.3.0)"] +rocksdb = ["rocksdict (>=0.3.2)", "rocksdict (>=0.3.24)"] +s3 = ["aioboto3 (>=13.3.0)", "types-aiobotocore-s3 (>=2.16.0)"] +valkey = ["valkey-glide (>=2.1.0)"] +vault = ["hvac (>=2.3.0)", "types-hvac (>=2.3.0)"] +wrappers-encryption = ["cryptography (>=45.0.0)"] + +[[package]] +name = "pyasn1" +version = "0.6.4" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b"}, + {file = "pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" @@ -2805,6 +5420,7 @@ files = [ [package.dependencies] annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} pydantic-core = "2.46.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -2945,6 +5561,31 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +description = "Extra Pydantic types." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1"}, + {file = "pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049"}, +] + +[package.dependencies] +pydantic = ">=2.5.2" +typing-extensions = "*" + +[package.extras] +all = ["cron-converter (>=1.2.2)", "pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<10)", "pycountry (>=23)", "pymongo (>=4.0.0,<5.0.0)", "python-ulid (>=1,<2)", "python-ulid (>=1,<4)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)", "uuid-utils (>=0.6.0)"] +cron = ["cron-converter (>=1.2.2)"] +pendulum = ["pendulum (>=3.0.0,<4.0.0)"] +phonenumbers = ["phonenumbers (>=8,<10)"] +pycountry = ["pycountry (>=23)"] +python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<4)"] +semver = ["semver (>=3.0.2)"] +uuid-utils = ["uuid-utils (>=0.6.0)"] + [[package]] name = "pydantic-settings" version = "2.15.0" @@ -3002,13 +5643,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" files = [ - {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, - {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, + {file = "pyjwt-2.15.0-py3-none-any.whl", hash = "sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8"}, + {file = "pyjwt-2.15.0.tar.gz", hash = "sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4"}, ] [package.dependencies] @@ -3058,6 +5699,62 @@ cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy docs = ["sphinx (<7)", "sphinx_rtd_theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +[[package]] +name = "pypdfium2" +version = "5.13.0" +description = "Python bindings to PDFium" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pypdfium2-5.13.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:882f4bbd4b17a335b43603169a14cde9341de12b238acd5c39e690cbca7c4293"}, + {file = "pypdfium2-5.13.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:d96929bde3bd64c771ab3558ca1ffd7704cc4d872ab92cd9f8f8b8a20f7f36b8"}, + {file = "pypdfium2-5.13.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:da5c7b74eebf40b5c1fbe1de01aa1edc8827a79fb1efd999616bc20dcaf77ba4"}, + {file = "pypdfium2-5.13.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:2abedfb5c70992b19c780ed58d7f7b929e8ce8ee52c9140158f44317c90ec6c7"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ee8c2bb2e68b396ab4a763215ac100dacb6b96d0da5bebeb239a021aecc3a7e"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f58e91b8c45ca144a1ff3008faf3c73ef8a5e9fb32988831788363288228cd"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46b2f5be9e7ae941ee4216e3d20b66f9dc3d81944a3d57756272de5275204709"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d96beb7f379e6c76d874ca93fcd182ac3168dd499056407070f9927fb1061b8e"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81df25c1ab4c13ff773102d3cbea1967511d079123b067fc077bd0c4d57d91d8"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a32d89fa5b4a2715810171239eb194df4aba604727483ab760512f3c6a851"}, + {file = "pypdfium2-5.13.0-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b90b0a5ac310bb34db8eb848e58fcab4e201e124e3cf3cb1ccb7b85293e034af"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ada81c36483cd61d07e32bc7814620ee96256b4f421b913f566861bf91800248"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3826e521e895648983cb9ee6b934d4bf51552600043984f84e9c2b3b14b696f3"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5c029d7163a91f264eafab51fb442a84a33efd9fd83d5a06c0136a7857a3cc8d"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:be2dccbde0ce7efe334ecd8f348df4308db360756ede4f0821d82dfc9a58caa8"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:bcd81394fe101405e026eedb3e40bef84635c1e5d974dd6036420eb6937753c6"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:2ed32ff685f8e05e637c990bedbf5fca66727bf27718d8bc33eeab21ce0630d1"}, + {file = "pypdfium2-5.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9c777edba28d1d5fd15435ed3a78ee2fdb93dd069be37cb53b559bc122793770"}, + {file = "pypdfium2-5.13.0-py3-none-win32.whl", hash = "sha256:d33ee7077db67478b75efe4b5ea9610fb96c5416a0bc4949227f0f59c34dfcd9"}, + {file = "pypdfium2-5.13.0-py3-none-win_amd64.whl", hash = "sha256:47dcca2a8d507b5fd24f94c3c9d48fb379430f097bc20f01beff6c963ffbcedb"}, + {file = "pypdfium2-5.13.0-py3-none-win_arm64.whl", hash = "sha256:554a0b23376460af1410e3c915906895e2dac67a086b9e6ccde0643a795d3b0d"}, + {file = "pypdfium2-5.13.0.tar.gz", hash = "sha256:7ca2d8e31bd8d0d40c496416b7d8bea423388669ffd494929f50e8c3a82326b8"}, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" +optional = false +python-versions = "*" +files = [ + {file = "pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273"}, + {file = "pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6"}, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pytest" version = "9.1.1" @@ -3157,6 +5854,34 @@ files = [ {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, ] +[[package]] +name = "python-pptx" +version = "1.0.2" +description = "Create, read, and update PowerPoint 2007+ (.pptx) files." +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba"}, + {file = "python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095"}, +] + +[package.dependencies] +lxml = ">=3.1.0" +Pillow = ">=3.3.2" +typing-extensions = ">=4.9.0" +XlsxWriter = ">=0.5.7" + +[[package]] +name = "pytz" +version = "2026.4" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2026.4-py2.py3-none-any.whl", hash = "sha256:9d514388fbc89ca0833203464272ac485b8828568ab73532f5020f17e892a0ff"}, + {file = "pytz-2026.4.tar.gz", hash = "sha256:464303645bafafd72418898368b2429458f709cf1eb6a15372fbcc396b64da63"}, +] + [[package]] name = "pywin32" version = "312" @@ -3187,6 +5912,17 @@ files = [ {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -3287,141 +6023,141 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.9.3" +version = "2026.9.10" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" files = [ - {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"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7dcad477c49c4c626a6c4fcd71b39a971aa217060cc40a6569fd24edcc0fa509"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d414c411c06fe0009eac33488fb1591c66b5c2673e342e452e7bb2fe63da8194"}, + {file = "regex-2026.9.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f2f43bf4e47ff7ce9e585558706d698c6204d0f80bf2207766382ed817c8e9f4"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d12a625a37c89c2b09303402a06942f55f071b95a7916a49c17034c3d47cd5"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef4ce69ff97fbb44b46751cfea5e859ad0b66d1a50abf34954f0645f51e81671"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:044bd4639b6bb409ec9e5d8b7accd57e02b4c4a4e2eafde916f8ae8006b3e40b"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d95d7d9538b5b726dd6fcd7b6117a71e6565202f6d64f5845fb4d8f203f533"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa911fbbcf8ac90bda0e0657d60768e3390954ef0588211d63a22add1cb1cd1"}, + {file = "regex-2026.9.10-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ecb2e7acb18f8cc4a67f0ad986c0af291ea4dd385d0614ba9bc09d7f8bbb478c"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4971776b4f2bd7fd9a83eceb2cb2592cbe2924f639fe8045e6a9de5ba4bfcf25"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f8bdec659a8fa7af51a32b224b3b7c02bc415d54ffd35187b1d224176b17d607"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3540734dbe241ebb3b87d5713781f6749a3e4d45480f506aa5fb5cbb0c37d249"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87f5f75c109f08f5c602d68e1af54cead8165189c727b6ac946b30b9833a3ba4"}, + {file = "regex-2026.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88b02aa8d0ec9b6189fe933d425775882271c23700ac11fd26d1779b0f56fde3"}, + {file = "regex-2026.9.10-cp310-cp310-win32.whl", hash = "sha256:13c52fc377792675f604a207a2ae5958c080f6854f7698d40d9ff034d95b1e76"}, + {file = "regex-2026.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:e6b99181d184d0f5c7b36b8d12b94d1e9499cce6246594331f9edc5d2ea9fceb"}, + {file = "regex-2026.9.10-cp310-cp310-win_arm64.whl", hash = "sha256:abbfc1c33bf8efddcc43844aba61e036d74a918680dc3ce8ce2538b004eda0f9"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:debc629e98b95abaea1cf3057ca296151f348c697c9b8a59d18013adb302c0dd"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d772586951d7d6a5d162d48f414065e483b1c81ab38fd8ed97c78b05883421a"}, + {file = "regex-2026.9.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b43456de605c8ee77eb75f07bc1ee44ba27f9cee22207deb77d495e954b7d953"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0a8b4928823bc8b217a1ab7bf3d90598909dec9a70fbbfe9a52cc4eca55990"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b91c37551bf39d75116c02b146956f65b9aa0337a4a652f4ae186983789d4001"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b9ba3b2765cdfe18f0f561a69f78a69701f2896654a81c711108d35d14e5099"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0acee94b480dd853e39434aa9a575f95385b1b4b8fa3feae56db363ca5cad782"}, + {file = "regex-2026.9.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a41693eb3fc4b92e6127d113813c6c395237f7edd3224abf67609af48c690d11"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6888065672b341e5246f391ec16dc258a29218ac784172fd67c30d941544755b"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8c07021a4faa3f092869adbd1f35cdc7a592276c807aeebc3ceb8ff1a638f0b4"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:58c01f7b81079cf0817ba831ff4d9eff5d28be4a3ac76c353e6f09bd63f4c386"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f0e2e5d23448b660d60a6ed85c46cc03b4b48bd276b8f4041d4a5fe2a4a0626b"}, + {file = "regex-2026.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94d096369b7cd96d15343fef5257fe39eff9d0e8758b92a0e15e358b92cdb2fc"}, + {file = "regex-2026.9.10-cp311-cp311-win32.whl", hash = "sha256:7f8f10015866608fe4c043cec2e4fe4c39a94bb50e45091de4cdf4004b9ae4b0"}, + {file = "regex-2026.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ce7c118cb102975f974585688357a717ffbf9dddd64ab0bb1bc93eb5b367cf95"}, + {file = "regex-2026.9.10-cp311-cp311-win_arm64.whl", hash = "sha256:1e954e246466d5a1a78f563ce8364b5d7cb19e7adb0ccdec8f9c9610083187bc"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a"}, + {file = "regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0"}, + {file = "regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0"}, + {file = "regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1"}, + {file = "regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348"}, + {file = "regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86"}, + {file = "regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da"}, + {file = "regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041"}, + {file = "regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7"}, + {file = "regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643"}, + {file = "regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5"}, + {file = "regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4"}, + {file = "regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468"}, + {file = "regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864"}, + {file = "regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb"}, + {file = "regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76"}, + {file = "regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405"}, + {file = "regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77"}, + {file = "regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd"}, + {file = "regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe"}, + {file = "regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7"}, + {file = "regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca"}, + {file = "regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873"}, + {file = "regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf"}, + {file = "regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2"}, + {file = "regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f"}, + {file = "regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487"}, + {file = "regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376"}, + {file = "regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb"}, + {file = "regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d"}, + {file = "regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f"}, + {file = "regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8"}, + {file = "regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548"}, + {file = "regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d"}, + {file = "regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f"}, + {file = "regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72"}, + {file = "regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c"}, + {file = "regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d"}, ] [[package]] @@ -3477,6 +6213,222 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rich-rst" +version = "2.1.0" +description = "A beautiful reStructuredText renderer for rich" +optional = false +python-versions = "*" +files = [ + {file = "rich_rst-2.1.0-py3-none-any.whl", hash = "sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255"}, + {file = "rich_rst-2.1.0.tar.gz", hash = "sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd"}, +] + +[package.dependencies] +pygments = ">=2.0.0" +rich = ">=12.0.0" + +[package.extras] +dev = ["mypy", "pre-commit", "ruff"] +docs = ["docutils", "sphinx", "sphinx_copybutton", "sphinx_rtd_theme"] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "rich-toolkit" +version = "0.20.5" +description = "Rich toolkit for building command-line applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rich_toolkit-0.20.5-py3-none-any.whl", hash = "sha256:e21fd616db9c0539d3c50f3433fb1173db95c19d3cbf215615e224182fad6c18"}, + {file = "rich_toolkit-0.20.5.tar.gz", hash = "sha256:0c9e1c414ffb0720be26285d472e263d1e704b71d31a7e13274b9996db4969e1"}, +] + +[package.dependencies] +click = ">=8.1.7" +rich = ">=13.7.1" +typing-extensions = ">=4.12.2" + +[[package]] +name = "rignore" +version = "0.8.1" +description = "Python Bindings for the ignore crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rignore-0.8.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b56052bff3aa1e6dacb42192be701efdbcc546c52f6302d4146ed93a31df4593"}, + {file = "rignore-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8625d98b9d83ef55f11c06155bf31cee21bab9c1fc001459a7cca878bf1e8c1"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a6b6664b3e9b403af1abcf81475782120b52e1f4063250638dce18a1856fb8"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2584771ef0e3e6c12f19bc50eafc1aff338e125efe0030ba0c3b29731faabf6d"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:773acea1c43241d5e96db86d52133f3098246337639a1f6f474ee8d5b304b36b"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a742a2c09a63bd65a52ed6047ba324593b6bf2080bfc570af199b00d617a7349"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c76ef4db664b9bde5da57f1cc58ed8545f9dfee864fd51eca21c60ebe9206dae"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:39478de64db6d233d1bdfca58fa8223a073a13f0facf992ba05a739a093fe4ae"}, + {file = "rignore-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2f0ea489c0e3439f1e94f2323e55218321b0a5f098dda4f1537e1caee12b035"}, + {file = "rignore-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7d38b47249754453e435d728ec019decee5ab450d50347025605484aa7526bdf"}, + {file = "rignore-0.8.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1524ca0df28b6c967fb9594a00cf1ba79e280dc513ec3e02d29345c79390e028"}, + {file = "rignore-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e0ac8e46f89a824326626f4023253cdb8dd09c55c65da51b9824200d261697a6"}, + {file = "rignore-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20743f8ef88aa6211f5ce3618c2dda34dd0afb4553e70ed7b18ef4cc1a0d8d9e"}, + {file = "rignore-0.8.1-cp310-cp310-win32.whl", hash = "sha256:b2cf5a1498d7f1a14fb5599e59805875cefbcd8b7d989e3b73f52829200ffc60"}, + {file = "rignore-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:54bd28cc90dc4c89c92a9191bc899e84786087bfb7c417ad0c29aeb90c2d0333"}, + {file = "rignore-0.8.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e4bc5ad282f7f82d9dcbb51b2e079f9a09c189e8c36017833e34003d253e0117"}, + {file = "rignore-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36381a30591b410b15d342279e1b3a3515e240a734c37d119cd0a9e6f71ef596"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2aa76742832db1fdac070fa8e694dda0daf6f2f7191832bd555d1e80f8c9c42b"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6c53ebb7ea2d22116d99ec155c5313c3d76220f06b4118c1e1de629de721a95"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0dbea2e28538fa64f8409085abf50240f1f7c5981a5479bbb1aea7b84d44aa45"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fe665768bfe8ddda77e5ef1c2332555ea2abecd33aad05fa41a19d959ba011"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9869b618dc28104003292f26cad26a2d54f6a3941684f16fe9ff435b5df1b0c0"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d59e4ee62d89136eb3f0f04db023c17f9294cde905cae2a0fc8d89a4ab86c57b"}, + {file = "rignore-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df64fa4f0b451cf198bcb1596bdce1d976396e579188141276d4ee8288ee2486"}, + {file = "rignore-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1fd861fe4c1fea39541e7c9e8a409472b9cf7604bb472fa9dc09c97bf7134b0"}, + {file = "rignore-0.8.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c49ea7bac11f3bed7af4a21fb76dd7820341103dd3d68676a7177d797262b84c"}, + {file = "rignore-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514f7be6d04a6f6be4835748b14bdb027799759d02ada03ead064a50c763b70d"}, + {file = "rignore-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b493c787dd2cc31b1e1c678e626bde593d83f9df420096f91dee6841ca317bfd"}, + {file = "rignore-0.8.1-cp311-cp311-win32.whl", hash = "sha256:d398c9e0d2a51ad16c296ea287a9e70d75715ff5dea124a7215228228cec8f22"}, + {file = "rignore-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:516ff2dba78230ed5a150db4fdfc0ad435a5a9137d27366e20a311001ca5a878"}, + {file = "rignore-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:13addb2a4805736a3c8adcd1c60871f517a4bd8b3a2b127e36d1e701924ccc0f"}, + {file = "rignore-0.8.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:487fb09f98c1e771f52b8433880c0012c25c5868db6e6f9f740ee0d3a13d4ae1"}, + {file = "rignore-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e7ba47a5bb25ad45d39983047ecfbcba01ecda0145458c548cd3f390b73bb7"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac0a7cadcf6154dd60b2f101425644179b34c540a88b84089995c1745e8c623e"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab53a0908a1f24d2aaf4201920932134fbeb19be1d4c9619514ef7c781c9f3cf"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd43540d294fbcb66daf66836b6c57043c0d3af672722099cd4a0c91448d948e"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b4316b266e88c25ac7b4a0c765aa93c436dfa689a29429bd276878db45e17153"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e68cfc4ee0a2909952af2aebd608d1cd22f7d1cdce332cb0b5ea3762939865d"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4d891dbe52b1aa4df22a69346e731ee3df219128956ca4391722773e6baab16c"}, + {file = "rignore-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:463f9734f06f2dd939c45f0e58d50c0648f799e9c41fb657e40d236054b14cfc"}, + {file = "rignore-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97fceadfe03f3b8cd77cceeed92d00fad67f3ab80a0fd4d31e8dec104b721018"}, + {file = "rignore-0.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:43342bf37e7bb57d69f766678c2b19fecf2b3ec757be3f1bb2fb774d2be8c81a"}, + {file = "rignore-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:514000eabd84e8f6f6a589a30d00e76d7cebe0d99b6cacede1223f64dca4b742"}, + {file = "rignore-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8dfa13e24fc32d3df33d40788cd04e07f6b607f3c5306808ee0c285d2135effe"}, + {file = "rignore-0.8.1-cp312-cp312-win32.whl", hash = "sha256:39b300b82e90f7a6ad102544b777f2aa8a26e9260ebb021399ee5a0a708a7c2d"}, + {file = "rignore-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:562055b620844654f0afff4df0a9c83f3679452c276cfa76cc5e49c14ddba481"}, + {file = "rignore-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:19606039e35dea7c1a8360f76fe39c5df6715d593a4f2e0c0c712b1b50985d10"}, + {file = "rignore-0.8.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:16b17b02d3e6ff3b88583af4809b64e14c77cf4de195763f063e5e77d70cd112"}, + {file = "rignore-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:58d7172259fd45d8ba00f3e02af5af2113f547f4a7fefab42cab567aa7c999fc"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c5ce3b10ce4b716abc535bc0fdd66b0b9fa9f5b1987b36d3f5ece7d0e2a9a81"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c199fa2d4a898e9b686d846371ead3c8e08d3e29f78e2ecabf4c580820a8f764"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7136b7ff29c37c8ec8effa3c59e27839482ba542af94fdc2581a59405e99037"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32b0fcd01495cc4f4d10b5307f9f15818c9db752cfbd7a9ecb00b0f129a70dc2"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca91c91a53135889945e77b286215ecd41c8f1090398db97ca3459c5a290eb6"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:67e30c0883f9aef3bcc45e5dc6980bc41f161da6c1b1a460f0bd87064c1d6594"}, + {file = "rignore-0.8.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3012fc79b19953f76a67b2e6bae5f456742e96f0ed33d3c6c9390b0586fc2da9"}, + {file = "rignore-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a437c870f1465aba36eb4ac7108c09d5097c436123fdaec980e7a26a4595141"}, + {file = "rignore-0.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:59f4f92ec5165619b3bc58130ab558bc618f9c5e9df06807339a85d965143fd5"}, + {file = "rignore-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:304ddf5f807c788c08a8210f9977131e7bfa9aa94704668d127689c00279ef41"}, + {file = "rignore-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ce5c6d8f02badc55112b014d4fc9af662f0649869913d25785d7b3676ac19ed"}, + {file = "rignore-0.8.1-cp313-cp313-win32.whl", hash = "sha256:711369d017f8959e3a253dedb76f6d2f43eb669a4228b2c1b796e30a5370ca36"}, + {file = "rignore-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:0585eafcacf15f8f2764b8d03a579761296e8c51dd0558f8eb59040810ba8404"}, + {file = "rignore-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8ddf078c4b5da0665a83f7b558dee2d746293ffcce3dfe8786015aed0aac4c2"}, + {file = "rignore-0.8.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3054fab93e2d2ad483cb89417f7b883cee025cb22286eb824af0073f2ffd5f7e"}, + {file = "rignore-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60f1ae9eaa51b50d5afe99bd25ffb2cfc10729701ca08c179489555d2614f3ae"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6012766ea3a5a635d9b79f3e8c3797d5e47ce5e5dd81993c9f92a3ea4ff68b4c"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90e60f0073caae0f1d59c993adedafa3a57bc6fac551669677cb25aa7fa9d9b8"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:121ab7ac93e39fd1d70098461c1ed9a6fb89d54e7bf8c60ae351b23b05cbb8c1"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:befb772556c8463c640b290f632b57440182edd39996708a33c50bcf437796f9"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a184fa45db8cdc7a8604d2df020a107be3fc0adc2e522b4e4eb5cd5b57d5f84"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e68a572efc126aa45195f1581a5ea97c4e36eeba6874a6635ae44daa4fbec7a4"}, + {file = "rignore-0.8.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c75ac1952ca8892422de328a925f4804120b149543042313bb3af7cbfdd65d64"}, + {file = "rignore-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3538084cef4a66ba3fee7c453d17db7cfb32a6653456381b62afd8d53090d6fa"}, + {file = "rignore-0.8.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:28a3baab3d1b7ea42c38e492d4a100de3e5c67217432364c62b5719e0f04e96a"}, + {file = "rignore-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:74996d8ed1a494ff8e61d9194a0dd3637e6b19a582cbed6fe0e1a4cc60e7b266"}, + {file = "rignore-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a2e5df9ba53e502f676b054c4d12371273204e2b9c6ec29afad3a155b9ad3399"}, + {file = "rignore-0.8.1-cp314-cp314-win32.whl", hash = "sha256:69db47c2fa51d88e93b4e4de4f44220ce7a9337e4b7526a16f5660b19974a8ca"}, + {file = "rignore-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:1c668a541ecd8af3d7bd06c48c62e1c3e0a755028d57c07cee697a25d9b0678d"}, + {file = "rignore-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:d63559131efa9cbba82494ec8698e38bea403f214969066c661b3eb4da4ad4f9"}, + {file = "rignore-0.8.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:b60f3751f681a12798927d51d1d6a46b81fe7f9923a513dd8fcc7947d785e12f"}, + {file = "rignore-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7d56bf138418e31991ce17b738c491fa1ac098bfcc8c3ba67fc0faeecfed357"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f020b018577d0081a2b23da199d22c58f4cbd63935c8e991adffbb1a755b467"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d4a70c9b857657c3a542fd367e16ae106fb6e3e4448cddb30e4edc604d5a025"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab668664e6388afc08fec50186ec21daf17c738ee71bb039d12cf5f14e964dde"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a3454cdd8bc145fd055dd45f650ccf1509e7b4edfc720152eb2be594b230f03"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:425b962f3d68b86ebb785409153708a38b0658542c918ee706ea433e2547c805"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:5094965794d163e1c4a71dccf3d1c3b3df86802e3afbfa461b300aa923cd7ea5"}, + {file = "rignore-0.8.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1467756e8454d3f816131bbad8b0efc52b7f6924e9b151f2962cec9e2f4af706"}, + {file = "rignore-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b5acf12993258f0eac4db81c30bed5469ebadd1b5bb2863c985fe0d802d94e0f"}, + {file = "rignore-0.8.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:329ae0bea9598541cb818bbecb1e1f56fc1faae77828ebb3846ef2b24a050d05"}, + {file = "rignore-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4069fcdff01999cd2d5a426eccda45a8b31a3e0eeee8b5a9da5a452714cbb2c7"}, + {file = "rignore-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf28097a83e1237f87e11e33fb12def022e103cab2a20340a19753e8e222aebf"}, + {file = "rignore-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:c978f26c25e66c118da1e6218f34cc5ce2bf9c5cc3969011cde47bacbfe4a072"}, + {file = "rignore-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:790e14dba9e1f8a532071d64e1c2fa7529c71db75996e95519d06d3378cbbcd5"}, + {file = "rignore-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:484c0a4803a1eb9097d875df3200e60e58cbf316d4d40357f92e8e757ce529dd"}, + {file = "rignore-0.8.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1c2a29e7f54e879fa54982887de480b2cfa6e3cb51f01d70dd4e4035f5b79670"}, + {file = "rignore-0.8.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:58449c827c36dba68133a5ee4137b67d7d6e050234a95c3a8f5e3cefbd6970ec"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e250726b08957aabcf4e28aef4ea18bcccbd95f240cebb75a74b424f067dce4b"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62470f4d70d83f124381975614ed7c7db6f5c5fb4d777ee88f7bdb9a7d14b65c"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d886d6bbdd0a1a3ef73bd38d6768cabc9b30bfd1be7b157b27a4ac7ec6c5244b"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ed31657a20df59b0bd63283d278e8678749aa0154ba98fae0838a9845599695"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e425a42f6601bfed266f76f3294bca48efcfdb10c3c0c279fb2f977b1e2cc2bc"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:ff09db59f672d929bca88ee7089d3697256967df76a2bab8b187208f2b517bc0"}, + {file = "rignore-0.8.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8dbc5898945c027dc0ae451de0c983e20bd8fa8297f40ee38d0ce1a3ed924d4c"}, + {file = "rignore-0.8.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1160abcd855964a9dd69f3d603ef57be14b1ea51ebaf50b07f737a3f3a8b89a6"}, + {file = "rignore-0.8.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:be994859b2cbbc69338351bc9908dd7d049de232e6eab5e998ec1feac3faf785"}, + {file = "rignore-0.8.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:f52098f3245c7557229d8253ea3a53a03de03704685aba8a1ccd24a5f004db70"}, + {file = "rignore-0.8.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c676080bb70cbd5429052bb5a3827b35543876a42c792c6d713d9e162bdaa00d"}, + {file = "rignore-0.8.1-cp315-cp315-win32.whl", hash = "sha256:ec800546b960d5044d2468d22dd0689210e846350c42cc9f02519c2a2528b3ef"}, + {file = "rignore-0.8.1-cp315-cp315-win_amd64.whl", hash = "sha256:4225f2d2f0b3e3c39a815f15dbe551f39f0d7f82e2a92d16bdbdb07ff4b6718e"}, + {file = "rignore-0.8.1-cp315-cp315-win_arm64.whl", hash = "sha256:42480707cdd5f92d6b1252faada600e03ab22fbfd53b124c6f7b33c21e06b6cf"}, + {file = "rignore-0.8.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:51e608acd3c1aa834f2b7d956ff5380a0a6b6b595df80e665f2a2889982f1855"}, + {file = "rignore-0.8.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6602011b6e6ecd157f3b0a0a25b1777b40c541ca21de6ebe2137af713d0efa15"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8db2be0d49175d5db4cd503f8dc62cbe687b9ebc71af4cede1c8b40bab8f4f4a"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:601ff49d8458a21d745908e35f792c02b45ecae4a37fa3cd1fc03fa065962bc0"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb58e93997d546fad4cfcd730a54d7c12cd7a61fe7a9b31bfc75402403dc559f"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0378cf77b8d64560e0cb433deada18438b36ef7933dd284dd65347d03c56c429"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0097a8c35106997d2b52851f0888777efe10e34772140a9fdb018b2f99238159"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:2a809a250f1532b93dcc52e173ee71adcafdb536a125532687666047c4537ac0"}, + {file = "rignore-0.8.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:580e019787032b430b857335a66db53d7ae0a200586b3a6b5d3ef227648300d6"}, + {file = "rignore-0.8.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f922dcf01e6a7ad26adfcb7f42635ae63066b883d409876129db62297061327e"}, + {file = "rignore-0.8.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4900bd1ba8938e5a5e601306504c7b8169783a2d44a8396c8fda4a9b659eed51"}, + {file = "rignore-0.8.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:3d581bf107490abae4d40a9aa823a79e18c88a21658514ccaf95e42e85d278f7"}, + {file = "rignore-0.8.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:78490b93aec14a87fa4c23e634b1d1dcb2bf5dcb90923f3b03a3428892a13d9f"}, + {file = "rignore-0.8.1-cp315-cp315t-win32.whl", hash = "sha256:e8315151a83b982fe972420372e80c550616095181d1549faaa1ec31f363f5ae"}, + {file = "rignore-0.8.1-cp315-cp315t-win_amd64.whl", hash = "sha256:69be37202a052d9e13affed6b774f86d8bf3b417b15d54ad737033e56da07ec0"}, + {file = "rignore-0.8.1-cp315-cp315t-win_arm64.whl", hash = "sha256:4268e83bcb88f240ed4f43d73e7de9e4bcd8026bdfcfa836567d6c29863e5361"}, + {file = "rignore-0.8.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8232b4a3e5a51f45f3bb8c53be7c96ac437bb238e414dfc37a0a2a42a6c90394"}, + {file = "rignore-0.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fc042ee2f3e3a38f6f44bd9f097c81ee94686897d3931f5e43d21789d34a81a0"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:916f8602d4dec7fc8b492883539b06fb3fea4f7b29ba8e4c300e77b21bdb8d28"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86f98b4481c8d60635e8f2c2d33b92d0b8c02d2a87eea49e19d7b25cde87a77e"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149b7a5cd9ece252ae28b386a979fac575546af5cb667a3f93148addfd8e6686"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87d0e03fde346256577b7aaafce44d593a7454995ea88c03005982c80890d6c8"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67a0ac8ceceb67fe9acb68ed16c29c30303395795c12c699c1af55e4af07410d"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_31_riscv64.whl", hash = "sha256:46aa37be9078fca572461a40b5832dd80aa34673d6e557d606feafe57072afe0"}, + {file = "rignore-0.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:45ec57389b855fa95b251205e3a8a1963a7185f28665c1a0b24e05d05012da89"}, + {file = "rignore-0.8.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b0ad0565d31aad6500a7ded6affb0165ae39560b6b056c6b179e5550ff3a887b"}, + {file = "rignore-0.8.1-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:c0a66b10f89af390b014e39db3f20489336eafa2521872b0c05017459721a420"}, + {file = "rignore-0.8.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:96ef71291f53bf43b85055c220697053c35173418aa1a5452ae71e86ef83d3b2"}, + {file = "rignore-0.8.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:75b9782c32434196c66f475cdf7be6f2a8c15c61c8c238cbe38b64657137f21a"}, + {file = "rignore-0.8.1-cp38-cp38-win32.whl", hash = "sha256:3b745c799e031dbad32aed9089d02f6c7be6024284311a5a76dc2f7d43635409"}, + {file = "rignore-0.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:0cbb7afa893d46ffc9a303a76d9058e0215ebf085c2e3100e31d2226f6a9bb7f"}, + {file = "rignore-0.8.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:092930871001f214069d03744225ccef7379a32a515e2f005f9f642a2ef34629"}, + {file = "rignore-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b1018f5ad66da4eee7f451db38a10444aae68952dda2afa52ad49207acf8490"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c4905de9800dd404084e2af0b97fddbf002e283663809c37d9b10dbea1c024"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cad647c6e0542219cfa421780e854cd1890936bda7096cfb9d859fc84eeb441"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc48dde27bf241e3982756edad000ca17da2e42bdf163919932f1e37d588f404"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d463ce689125a3e311489d1b1b39b8471fe0d6e76b8afe9e26b40766a3fac19"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c51c54dd953d18b6cefd5c6c70913aa87567efbead5c1f34005931305f9c53d"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:baf3188d642019c0b9e93230cdca140d2a46dd846544d10c19a7c857dd953eea"}, + {file = "rignore-0.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0089e02bc87182e92b0f676659c748edc0378a390510ee0971e14d60b4e53f78"}, + {file = "rignore-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66623b4b19027202abef32ffdba74b17722b63f9b2024a6a2d660eb0c4f4abc8"}, + {file = "rignore-0.8.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4fb05a077bb4b90b51c3fc7ffd4110c3003a90ccf6ec1a8f8a4b2c017a3e8442"}, + {file = "rignore-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:831c9e863acb90176a47db90e55b33f1e6305206d708d784a0161bdf87b8ae5a"}, + {file = "rignore-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98028ec3e74c216fbba4a01538b7b440fdd6a908ad330dcbacbe591bcc06bb2b"}, + {file = "rignore-0.8.1-cp39-cp39-win32.whl", hash = "sha256:7c1e95cc854f8666383905582d879c1dd3275a983fe2e312e399c02c90fcd0e8"}, + {file = "rignore-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:f109c7f0c579a8fe795300d830eea53e5e28a96c6f819686c33f7d4afaa9c9b5"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6e658a26627f27e15ec54027f051c50818c86b60f940cc20991054e00f5c0152"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1867356484fdc502695f71c2c986cde702e2a14d6db76ca3837a6f066cafb93c"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a82567032d81f559ba4a9f3e96ffcb48ae149dff2f44c58c966b5890eb248895"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9a9812dc25dcfb9bf2e546247f6e38b0c29a0d39864eb62de2a48b2ba41b961"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c735c5586f4306d9a62ad374093d6127b24ddadeaac322684e3e4c923eeb6a58"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a249e142d2d313872e526949b9cdc5363041d64531f53553e702c00747225dc2"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe7a7dcd84affbf312336991202c11922c13aff323e2857c5baf10afc0dd7f2d"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:08acabc3203c68a8bed7689c50b546a4ad7c9144ced2199c1261a149f7a3cef2"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4862e015736af87a4afa1a75023d28363ee34449928f8f5b521d1f9eabb0c826"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c96c3a434b50a7663e011cf6060b0f9ba44fd956b310b230bbca9dd665bb1f53"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:4cc3003a462778a75aadabde87edebc0e1d205c119c703c155bb03a4f3e8be23"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0dc7765f94d6660574d648c89e28c9932248499d47dad49c2786f2e49231e1d9"}, + {file = "rignore-0.8.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7b929db3f8f3fa240b0f328453de0b1b747e51b7992a9198c8412d16f4f0027"}, + {file = "rignore-0.8.1.tar.gz", hash = "sha256:2b6cf58501e9ff1b6a71c3fd66c8a105311e1f23237626fd4c9c00606bb3d30f"}, +] + [[package]] name = "rpds-py" version = "2026.6.3" @@ -3604,29 +6556,29 @@ files = [ [[package]] name = "ruff" -version = "0.16.5" +version = "0.16.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {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"}, + {file = "ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96"}, + {file = "ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67"}, + {file = "ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728"}, + {file = "ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee"}, + {file = "ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588"}, + {file = "ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385"}, + {file = "ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b"}, + {file = "ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284"}, + {file = "ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161"}, ] [[package]] @@ -3646,6 +6598,84 @@ botocore = ">=1.37.4,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "sentry-sdk" +version = "2.70.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "sentry_sdk-2.70.0-py3-none-any.whl", hash = "sha256:89600e8bb5fc5239cad5c4434217f453204f2207ad29399ea58947460b37a62e"}, + {file = "sentry_sdk-2.70.0.tar.gz", hash = "sha256:27e4b512f47be93136645dbe8f02d430473ee73a389ce430b2b7104baa66cf75"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.11" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] +arq = ["arq (>=0.23)"] +asyncio = ["httpcore[asyncio] (==1.*)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +google-genai = ["google-genai (>=1.29.0)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +langgraph = ["langgraph (>=0.6.6)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] +litellm = ["litellm (>=1.77.5,!=1.82.7,!=1.82.8)"] +litestar = ["litestar (>=2.0.0)"] +loguru = ["loguru (>=0.5)"] +mcp = ["mcp (>=1.15.0)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] +opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure_eval"] +pydantic-ai = ["pydantic-ai (>=1.0.0)"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] +tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] + [[package]] name = "shellingham" version = "1.5.4" @@ -3692,24 +6722,120 @@ files = [ [[package]] name = "soupsieve" -version = "2.9.2" +version = "2.10" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.10" files = [ - {file = "soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823"}, - {file = "soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74"}, + {file = "soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502"}, + {file = "soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.54" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sqlalchemy-2.0.54-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24ae093dec196ba37fc2beb0316de53e7871d3d246a50faecbbb53034e41ded2"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cc6532f930c27974e9239e5ce5abebe7600ba9807cea4fcf42f1b6cab18fe7"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e7a76d5dce712ce50435d0f97181eb955ec27d138c004176f01282e063bac52"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f5c09090b1a7c4d389d1431f820931e8df318f82caafc53f9a72c872fef467c5"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:762cfe4d340c56368256d936a98b620a9a5650e49c1c84eba51d6edd17ffefb2"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-win32.whl", hash = "sha256:6b6d4e601c4f6d85e99bb3416107cc9418c5603ca73d4ee0f5f8d79c2a1ed9e8"}, + {file = "sqlalchemy-2.0.54-cp310-cp310-win_amd64.whl", hash = "sha256:03cbf8d9a67da618bd65500a5eb3ddac89caf4c61e99b2f03fa4a1952a0725a9"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d03084f3352dd92048cb19c71d90f116d076c9c7937e0ebc7752c4685de6d38"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92622fbbda1b1fe1632f3402a6e516a93c0e41d9158839c6b3dfb12117f26b72"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5800ddea045c2c860ef1d359a07a3066c7c0c426f45e3abc3874e116cb3c6937"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019abef05a4b5eafc8eae6fb483167fa28a4dbe5f518d577b744f31a5276a37"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b67749f7da3985a529cefbb1474783cb91ef44371cb9713630bade3de908760d"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-win32.whl", hash = "sha256:2f61a70b3b82e2ec7ad6a4f2301422b9ca93ff06917983e41317bcae878bddf6"}, + {file = "sqlalchemy-2.0.54-cp311-cp311-win_amd64.whl", hash = "sha256:1d887fbd5d248e250807bd801e697fc73e3b44866ce5f093dbc90512e75bde25"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e"}, + {file = "sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c"}, + {file = "sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44"}, + {file = "sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51"}, + {file = "sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:330d35f9ce815d35cb1daab038d4d7ec0e907f4d7ed0fc8bcb2411d1f23d0b50"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1f455db400289f77ba2f7b62fffafe8875153812d0e3777aa4ff2b34a0fc1f7"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8a4afcc7d714cc3c8a57facdff4c3529f5f93d71e54b7da1e03e022c9089c9"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:34e10af7d274a5c4b7cd0fced5e7361008c5e07d97dd48a93852d5b2f1142a1c"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7108f410f596c5ac22fe43ba467e864d27c4e1477ae89e90c6c87120b2c1be23"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-win32.whl", hash = "sha256:c1a3455a88f66e4851792bedb098ed942912253d31caed1dbc58afbfa9e875cd"}, + {file = "sqlalchemy-2.0.54-cp38-cp38-win_amd64.whl", hash = "sha256:f3ea33bcf0aa599c1511fe5c9fb126f45aa450419084c4823f786155fe4c79f1"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6c419c83a87fd901f0b1b5338ffcb82471c3ac32a86bb8883688c18f8eb85d3"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415239eb2ddbbc508ba4cac97affb91c0f210548fd1731edda6e529b0bb93015"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:279bde5bfedb0f3e0f1bdbcffa2daa39c6c54d90f9408ef3b1802001597199f0"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b973e4facc2f80e42f5a27b841feb7e202661881a6320580abbe597a28a007f"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:deeab253fe01a770f634c7007c73702df2324c868a79ae756507a9a1a76294fe"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-win32.whl", hash = "sha256:d566099d60cded87d175d4171dc899b9613d2e3b663573364565ca1b27ccd241"}, + {file = "sqlalchemy-2.0.54-cp39-cp39-win_amd64.whl", hash = "sha256:744fb219a390561a57dbbd59cd69a22b5b5b2facfde794c1f79236dd847fa67a"}, + {file = "sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2"}, + {file = "sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b"}, ] +[package.dependencies] +greenlet = {version = ">=1", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.12)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "sse-starlette" -version = "3.4.8" +version = "3.4.11" description = "SSE plugin for Starlette" optional = false python-versions = ">=3.10" files = [ - {file = "sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d"}, - {file = "sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c"}, + {file = "sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453"}, + {file = "sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade"}, ] [package.dependencies] @@ -3725,21 +6851,38 @@ uvicorn = ["uvicorn (>=0.34.0)"] [[package]] name = "starlette" -version = "1.6.0" +version = "1.7.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.10" files = [ - {file = "starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"}, - {file = "starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b"}, + {file = "starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e"}, + {file = "starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d"}, ] [package.dependencies] -anyio = ">=3.6.2,<5" +anyio = ">=4.0.0,<5" typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} [package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "opentelemetry-api", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "sympy" +version = "1.14.0" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, +] + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "tenacity" @@ -3838,28 +6981,28 @@ blobfile = ["blobfile (>=3)"] [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.23.2" description = "" optional = false python-versions = ">=3.10" files = [ - {file = "tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224"}, - {file = "tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4"}, - {file = "tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a"}, - {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e"}, - {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288"}, - {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4"}, - {file = "tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96"}, - {file = "tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948"}, - {file = "tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7"}, - {file = "tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9"}, - {file = "tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa"}, + {file = "tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90"}, + {file = "tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305"}, + {file = "tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78"}, + {file = "tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40"}, + {file = "tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835"}, + {file = "tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef"}, + {file = "tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718"}, + {file = "tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a"}, + {file = "tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde"}, + {file = "tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa"}, + {file = "tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac"}, ] [package.dependencies] @@ -3870,15 +7013,36 @@ dev = ["tokenizers[testing]"] docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] testing = ["datasets", "numpy", "pytest", "pytest-asyncio", "requests", "ruff", "ty"] +[[package]] +name = "toktok-rs" +version = "0.1.4" +description = "Fast, exact BPE tokenizer for OpenAI encodings — Rust core, tiktoken-identical ids" +optional = false +python-versions = ">=3.11" +files = [ + {file = "toktok_rs-0.1.4-cp311-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d52814fa973c0dec408acc80797ddba12cdc26aa0cc9b2fe58c30458f06e7590"}, + {file = "toktok_rs-0.1.4-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f738105be418632a34d144e812803e513ba1e41228aa1f13656b5d6642b19fb1"}, + {file = "toktok_rs-0.1.4-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43ab1d12f91faada33eb59648088e294bdb1e235cc31a0a37ce7f6b1c950aebf"}, + {file = "toktok_rs-0.1.4-cp311-abi3-win_amd64.whl", hash = "sha256:76b3bc9cb6520d7bd8ddc05e1677fa15fe48c6fdc30c606a3261c5a9122fc553"}, + {file = "toktok_rs-0.1.4-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e99175c2070ac7dc321010afd0a6209d665257a7790bce79266e4b0209d8b5fe"}, + {file = "toktok_rs-0.1.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e64c33de11a5b2157117f04d18a8700ab08d701b302595a1d3cb40f518c59546"}, + {file = "toktok_rs-0.1.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:59c8266118a7f6d6a66bf9f4dc103eeb9ed05956bd2f24a6c7b491b4c7bd1c45"}, + {file = "toktok_rs-0.1.4-cp314-cp314t-win_amd64.whl", hash = "sha256:d701e1cb30bbd5e4948d1d9a8a58703d49cfe87e0877b94207f6f45b62d1bc69"}, + {file = "toktok_rs-0.1.4.tar.gz", hash = "sha256:5669460cae0c765c6183a648aeb0f12bb62743f1df450f90ab051e62af83c426"}, +] + +[package.extras] +numpy = ["numpy (>=1.21)"] + [[package]] name = "tqdm" -version = "4.70.0" +version = "4.70.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.8" files = [ - {file = "tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953"}, - {file = "tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220"}, + {file = "tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73"}, + {file = "tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4"}, ] [package.dependencies] @@ -4149,6 +7313,17 @@ files = [ [package.extras] core = ["tree-sitter (>=0.23,<1.0)"] +[[package]] +name = "truststore" +version = "0.10.4" +description = "Verify certificates using native system trust stores" +optional = false +python-versions = ">=3.10" +files = [ + {file = "truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981"}, + {file = "truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301"}, +] + [[package]] name = "typer" version = "0.27.2" @@ -4166,6 +7341,20 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} rich = ">=13.8.0" shellingham = ">=1.3.0" +[[package]] +name = "types-requests" +version = "2.33.0.20260906" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.10" +files = [ + {file = "types_requests-2.33.0.20260906-py3-none-any.whl", hash = "sha256:9f53622652bd921ead7a54d665be1b8d518165c8b51cc9d68d944cd6b6bfa8fb"}, + {file = "types_requests-2.33.0.20260906.tar.gz", hash = "sha256:76ab8a0fb736744a0c3deee7aa57b2927e301f078d9e61f5391b3e92002416b9"}, +] + +[package.dependencies] +urllib3 = ">=2" + [[package]] name = "typing-extensions" version = "4.16.0" @@ -4193,24 +7382,53 @@ typing-extensions = ">=4.15.0" [[package]] name = "tzdata" -version = "2026.3" +version = "2026.4" 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"}, + {file = "tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81"}, + {file = "tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79"}, +] + +[[package]] +name = "tzlocal" +version = "5.4.4" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.10" +files = [ + {file = "tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15"}, + {file = "tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["zest.releaser"] +testing = ["check_manifest", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "ruff"] + +[[package]] +name = "uncalled-for" +version = "0.4.0" +description = "Async dependency injection for Python functions" +optional = false +python-versions = ">=3.10" +files = [ + {file = "uncalled_for-0.4.0-py3-none-any.whl", hash = "sha256:16c4bb3337532e4bd5569adc192285976f3ad5305402256d34c67a12b5c968bd"}, + {file = "uncalled_for-0.4.0.tar.gz", hash = "sha256:335b95bd2422332ec210d518f314a16e4c640921c39fc8bf2ad095bd3538f4af"}, ] [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, + {file = "urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3"}, + {file = "urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63"}, ] [package.extras] @@ -4221,133 +7439,582 @@ zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "uvicorn" -version = "0.52.4" +version = "0.54.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" files = [ - {file = "uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1"}, - {file = "uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86"}, + {file = "uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf"}, + {file = "uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620"}, ] [package.dependencies] click = ">=7.0" h11 = ">=0.8" +httptools = {version = ">=0.8.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.20", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=13.0", optional = true, markers = "extra == \"standard\""} [package.extras] standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.20)", "websockets (>=13.0)"] +[[package]] +name = "uvloop" +version = "0.22.1" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, + {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] + +[[package]] +name = "watchfiles" +version = "1.3.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.10" +files = [ + {file = "watchfiles-1.3.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:000b9688fc8133037a8b075c8ebf98f32844ff8964dda61e85c1db547dafc441"}, + {file = "watchfiles-1.3.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bbc1198edfdc90fda0600f825aa94150f428dfcbf8138746f55998e0e660d64c"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af9c464d410e8c44b58ddf1cbec7c3a02c1cb2c6c80df9c74659ba2fbda0ab5"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb94e40b9a0db19636b5e3fed0a9803452ecc7381df13b385272abb779ee83ca"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7398930a2b76b9dc67a4e6b8bfc7baf30946892a86f417a3920c07bdd6febc30"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:025b108f2d5cc2799cb941f79380df3e8b22bfb589c218e801767fe78d598c95"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9ef3cd403d756a304a0a08a112163c63da7ec72c8c9d083c98818291ff8f29"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5768b49e426fd5b550b012c866db347cdf15c398ef98dc557b6e6b72fa74cd1"}, + {file = "watchfiles-1.3.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:d36c72fcdc08f143d87316a4c4c3b064e340da2c7383d39803950740ed418935"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c9865240e065a2247f4b5248528ae3d01e0e8ae071462c250c884dc16e3ee3"}, + {file = "watchfiles-1.3.0-cp310-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:380f513d26cc2e598266b88d66456e07d67ef4be8f0f435a1154d9c44b43d509"}, + {file = "watchfiles-1.3.0-cp310-abi3-win32.whl", hash = "sha256:509d9f74d2bec5c1f4cd868ddcfe0c9601f7ea53066dfcb19d8ede742c4de661"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_amd64.whl", hash = "sha256:1acabde19b67e673274e89a04d613b7fb1f122e2c4b8ec9a581e59c149264b61"}, + {file = "watchfiles-1.3.0-cp310-abi3-win_arm64.whl", hash = "sha256:d978cc1dd7ba44f5590d7de74f7a9c8be7262d9e469c2c0efb8bbfc2574321dc"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e0204f90677b7fb8d3a824279a71a01166bea9f6e53923b8056592d87b33b0b6"}, + {file = "watchfiles-1.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c1125e99f84934a92996ae343501285617373acd66f16b59fd5654b27ad8d80"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a85e243c87109d9f48a2e0d10d6abdee8768cc114848b32281ec83d7f81d86ef"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd538c71766c59c732e2c5cedc39323c99c11897b99f93eac773438e54af206a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da213822ec9082b62cadf3008e07a8c690ec7a3c89f6dc5e55a8cf07b56a1b26"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4593392e87669836670f87d62fc754059eaeb0e6157af44060a46558cebe5a3"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26b81bc515a0f03f66a69a6fea86eea54c8eca4173c46dd7222569f79dd2f977"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca39661934749df580d89f3dde266f2af7c6968f4c21c72ce2dc387d299e6aa7"}, + {file = "watchfiles-1.3.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:04c5500faa725d69a99b0f63750e80795e455bb951849e9c98a811b4324997c4"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ec3cd4bb181a7b6329134204c2277c7144ab5f62cce9c5335d6f901ee7ce8d6a"}, + {file = "watchfiles-1.3.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e5871d4a7f788a9f64fb06f5793389d29d171e5c32d37c3ebfd520b017cc7694"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win32.whl", hash = "sha256:81c989d7267bfb7b676cd32c83bec97914e396870983cbf493e29415037bff5e"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d754e049006e81a9be49dc78ba7802081cee426e968cbc63ee19e164fa2d0e39"}, + {file = "watchfiles-1.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a5b631db08fd3032db57ec6a92d1777cd60f88803287e4e941a4858c53aab1e0"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:45726c6b5a8d67c8087eb2933c4398948546d04206ef40f629dc4881d65d23d1"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6398d250994baff581147ccba846223076fe6492ca93c0d1b3588fcf5355eb5c"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:591de392458bca26f6130fea4e667c06b657e873ad334ea06fad5a79f5086cef"}, + {file = "watchfiles-1.3.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec46a1ff4c1d81405a5fc645abdbff5e4e9388f7c9a6cdb392316a7c29b884f"}, + {file = "watchfiles-1.3.0.tar.gz", hash = "sha256:99aee4a07847c06820765fd7b1b49ceac4f3f711ccb7d104655a33231de1c207"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "16.1.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49ae99bdfcae803a885c926bf14f886196e84925395bb3f568fef5c0f0979d7d"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5bfd1ac19b1b9986a9c95a82d5e23a391ebb09e12c34d7be6094b86efcc35731"}, + {file = "websockets-16.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9246a0d063cfcbcc85f2359dd6876d681213f4790832272aa16641b4ed5d64d4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1214e673c404684b9bf7154f5cf43b45025b1a6160fac3a9e438e9c1a97e22cb"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90001d893bc368e302ef168d82130b4e4fdd27b85fa094682df9b667c2d48838"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:130937b167a52af203c8d58e78d67705874e82759862e3b9671a452fec4abc87"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c9f23004a3d40e89c01a7955d186a6cc83418d93b749701944ce2de3e95a1f3"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f55f0b01956a094c8587146d9558c91937e78789c333860ffaf35931a6e5dbc4"}, + {file = "websockets-16.1.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6aaface73b9c71974c6497366d8b9628357f6c9749e09c4ea3610176c63f2ae3"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc0fad4933f427acd5b1cec210f3ea6dce7089e1724e4b9ec6ef47c6c04d1b3b"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2769a0344a09e9ccf5b3cce538bc75a51b53eff3275d3896310c8552049195d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f70541f3104339f59f830522d94ebadb1bf47426287381623443d8bb1cdbf33d"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:dc385593a42e31cd6fb60c19f0ecb015b386603818fc2c6c274fb42bd2bb4165"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:387e8e4aa5df2f90b198fa3cad3478822a89cf905b6a6d6c97dc3664689640cc"}, + {file = "websockets-16.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fd46fff7eb62c24804d234f0051c7a8ea81285ad63e0337d3dcf33ca82aee58a"}, + {file = "websockets-16.1.1-cp310-cp310-win32.whl", hash = "sha256:7883388947767080f094950b342b30d35a2a06b849cd967c422fa0db72b40ea9"}, + {file = "websockets-16.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d57685547e0060cc6fd90ee6a28405d6bd395e525545f13c8d7cd99c78afd79f"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a"}, + {file = "websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512"}, + {file = "websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be"}, + {file = "websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81"}, + {file = "websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57"}, + {file = "websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b"}, + {file = "websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847"}, + {file = "websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383"}, + {file = "websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3"}, + {file = "websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747"}, + {file = "websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df"}, + {file = "websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49"}, + {file = "websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e"}, + {file = "websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87"}, + {file = "websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea"}, + {file = "websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293"}, + {file = "websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3"}, + {file = "websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985"}, + {file = "websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9"}, + {file = "websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328"}, + {file = "websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999"}, + {file = "websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43"}, + {file = "websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217"}, + {file = "websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737"}, + {file = "websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7"}, + {file = "websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d"}, + {file = "websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5"}, + {file = "websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3"}, + {file = "websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57"}, +] + +[[package]] +name = "winloop" +version = "0.6.4" +description = "Windows version of uvloop" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "winloop-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:5339f42586c9817e510a05c83f9fea8ff966a25ab499e73ef9d523cb9a3814de"}, + {file = "winloop-0.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:2b4b717e65b0c9c45fb1fcb3ebb7d12191872084088c6ca19477a38b89d72506"}, + {file = "winloop-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:6871e87acb7211688b34df71647673ff67e58ca76887b39cfd1e04624b06ca54"}, + {file = "winloop-0.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:95580d21d951815eea3401113cd1e30228cde5b227c4f0c9b7d8a6973e799f39"}, + {file = "winloop-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:4670143d095948af5a68440deb275c73b602bb4df14e575e9628562fb1149a52"}, + {file = "winloop-0.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:543469da05f16d39f458df16bf8ac9a7da302342ac962de1f0192508e2bd4256"}, + {file = "winloop-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:ef6f2dfe5bf662791c2f32086e57dd788aa22d614881a6a3653b916d668fa70f"}, + {file = "winloop-0.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:4520ab3d501d54176fa28b1b41527ecfe6773e5148ff8d719d0be9b3b3b1414b"}, + {file = "winloop-0.6.4-cp314-cp314-win_amd64.whl", hash = "sha256:13b391ee3897630cec2391075641096fb7e5d09c3a92d3008f110eeeea32eeaf"}, + {file = "winloop-0.6.4-cp314-cp314-win_arm64.whl", hash = "sha256:c16df8143a9f9d1d1f7c51bc947720999db58d4d5b936b86b7bc9258de528fe8"}, + {file = "winloop-0.6.4-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3c95c8b07637ce3bcd50b69903f8933f6ab1ef0da9548f8f567dcdf0f763e"}, + {file = "winloop-0.6.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d5d642e5635dc98653a52dbe85a1048068cdf797c9a42b4bf4b9f6a0f7226562"}, + {file = "winloop-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:1cf8a5be289bf5f0536270c5eba7258804efe76442c8698433830c863a34e3e2"}, + {file = "winloop-0.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:1d54beb8420893258605ba1e9c11087d3038ac20bce165c4db306fd50a14b5f2"}, + {file = "winloop-0.6.4.tar.gz", hash = "sha256:20a5de55b5be63ed559d041a7b2256b67bd42f3cf3a118d8c7a71e5442c8e9e9"}, +] + +[package.extras] +dev = ["Cython (==3.3.0)", "packaging", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<9.2.0)", "sphinx_rtd_theme (>=0.5.2,<3.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5,<8)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0,<26.5)", "pycodestyle (>=2.9,<2.15)"] + +[[package]] +name = "wrapt" +version = "2.4.1" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.9" +files = [ + {file = "wrapt-2.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7aaff952dd6930fc87b52d44bf72ac8426e45e90058fe0f2da446889738eff44"}, + {file = "wrapt-2.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d67b916f2f777a6d31067e842598e059f1a0e29022c86454564fc9efb9a0c19"}, + {file = "wrapt-2.4.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2945f4bce1518e09eb25458dbd09800f7aa3fee81a5a5cefe66c4e82fe8cb69c"}, + {file = "wrapt-2.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7ce62ee24eddb76c317ec790c44f4fbe9f34ee0a5dae37b30c65a2b8f3e3604"}, + {file = "wrapt-2.4.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cbee8d00840ac89846f6567c75506260a15fbb4dc74de180678fafe09458d47"}, + {file = "wrapt-2.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b78dd8d058468156003ee8c212dab334b6eea18cd9c5db3b81f6c90a9408afcc"}, + {file = "wrapt-2.4.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4d48f1720569b4f6e2df97783fd270554abc4c6ebd74b2e5f15e5110283aaf78"}, + {file = "wrapt-2.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:de5d2d7f12557c9e994b4037330efa5700fccb9f2b9df0bd4ccf384eaf73c254"}, + {file = "wrapt-2.4.1-cp310-cp310-win32.whl", hash = "sha256:5c0217b8c12952bf4d137a19ff2c61cc9b626f52c09c43f0289769c9f5f808aa"}, + {file = "wrapt-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:0aacc1af512e040fb0b3c4593a8bded9aaa04235025f08a54eda9d8c1103d3c6"}, + {file = "wrapt-2.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:6f32a45d0e883918387aabf5384952e5a93ae40be0a6fb5f1eddc8281af4c714"}, + {file = "wrapt-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b6ea396edcfb305698f44ef0a47b9adaca15d30386ce093ca606844f776d7236"}, + {file = "wrapt-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b86bfe741840cec6d8d5858b1eb53a325f564f08d7e6fb5694981961edcec230"}, + {file = "wrapt-2.4.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ec7aeb92e956810eedc268fe75f8c11f05d6c3d864d0995a53eb6623b3b6a51"}, + {file = "wrapt-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba66eccadd4b857a845e309c08457bb71fa8300000e508e3a86cd695bf9c7515"}, + {file = "wrapt-2.4.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d5c8f3b4eae814213d4097f134be03aeb23816e6358213454ec5e62e8449fb6"}, + {file = "wrapt-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3bc7ab495e564449f39db2f54c1033d7190c944736d525f3bd06cefaf0134df0"}, + {file = "wrapt-2.4.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2e2e694057bcfd46f43e28f7b50fad8f5fec7113cc6445fdf251219539a08f28"}, + {file = "wrapt-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5577fdc702427698c81c8c9c3b8479cbecb3dfd0f697be8c1d09c89f5838cc"}, + {file = "wrapt-2.4.1-cp311-cp311-win32.whl", hash = "sha256:e78c0f6d66a564bab245faf170d55595df6fc237117b99d97fd838678f0906f0"}, + {file = "wrapt-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:1f1851be0e593d65e68d7c1bc9d3b971e19fefa219f617e490d75e80e467ee14"}, + {file = "wrapt-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:7ff549316d94c404999f96bb0a6993222566231ff4bede1b6f68908d1958a08f"}, + {file = "wrapt-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7e86fbc2ac8a363ea04abf631fad82720e16b17a25020f32dbe9b24a2ed2b0e3"}, + {file = "wrapt-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24389748f0b9d5b67e478fad4fc8b3f1108422ef80716e48eead6cebcebbff08"}, + {file = "wrapt-2.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30d11c289b013bf384ff1a1a6553f150d0b855901708a9bef667a5680f8247c9"}, + {file = "wrapt-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9356dbb59199a0e4709de35fa4a1ac1a88ef6da99711a397f5b009233faff326"}, + {file = "wrapt-2.4.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8342f332dada211f64b74609e332d727b13315e9a83177f7918bf68c59f815f2"}, + {file = "wrapt-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f86e328c482bc5383b4eda5094be0bed3617fc3076aa9225ff1a9eb6372de9b"}, + {file = "wrapt-2.4.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4dc92697444ee380544fbb43c86612d8486529aadf917c22b5524141d4af074c"}, + {file = "wrapt-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edd03758a7578526642508b8833d43496fdfba0f64e0025dfca153a7c1777735"}, + {file = "wrapt-2.4.1-cp312-cp312-win32.whl", hash = "sha256:5d83e412665aeb1e854eefbf1564d0d67872d9994b502a0bce96e6ff7f4970b7"}, + {file = "wrapt-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:b4e7efdd476ac631a0181551fd9aace844765ea3ce2b5133b194fae4421e8ad0"}, + {file = "wrapt-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:38819761401baa2d11916d7265b82f23265f8fe5a31c431dd7c24a8863c65f88"}, + {file = "wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3"}, + {file = "wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145"}, + {file = "wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc"}, + {file = "wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1"}, + {file = "wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d"}, + {file = "wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697"}, + {file = "wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73"}, + {file = "wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4"}, + {file = "wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4"}, + {file = "wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca"}, + {file = "wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f"}, + {file = "wrapt-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6c35541cc729964c65c2b9b1f9cf317811039abc2da13b4557f20f21cdc292b"}, + {file = "wrapt-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6b9df84f0a96763159cccb8e3b0ed83cc950c7c8bf82d6e45428372a805b3224"}, + {file = "wrapt-2.4.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78af62413095d0a57077606654ce85e273deda8e2bfa28fdb04257b962d94ef2"}, + {file = "wrapt-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60702ebc914d0bb01aa48f5c1785ceaaaa505e0a841b444a69d6ceb5de4097e"}, + {file = "wrapt-2.4.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8c4b44e4be7680fc496816e824b6ed134d781c4da296e44b75399196e5c248f1"}, + {file = "wrapt-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fb5b3f94258bcf71db902795f4a71151c7a74d9fe21fffa00752d2bd286866f8"}, + {file = "wrapt-2.4.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f1556a96b20d5bfdc9cb5dcd0a3ec65cbbac8a4eebcd3cd34efdc91c9519f660"}, + {file = "wrapt-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:094a606d0bf1c4b847b3a743d22fc69cb164015b8c70cf6f20a534d30889ed47"}, + {file = "wrapt-2.4.1-cp314-cp314-win32.whl", hash = "sha256:b0ee076be124406a7f97ca663c4a3ba32b6bcdd9102ca82497feaca79e9cb33c"}, + {file = "wrapt-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:d2d6f9abaa52de05090a2b4a4c1d0e858a268c4de69eec277506af9fbcccff91"}, + {file = "wrapt-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:3152b2e94d733a9bd70dbd1c95f148266f079a70e9ffca2a9c5dc421ade1b4e6"}, + {file = "wrapt-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:37ff91b390400463ecd4d080ea823314510b6843ac53e6e35cc09bba1805d466"}, + {file = "wrapt-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e0a518cac3e789443af54fc77f23e66f17d80192c281b160b42058f11fabccc5"}, + {file = "wrapt-2.4.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ecdeb8a1ec13397421e6f925412186bd87ab86d63517e6825b6d7bccf781f26"}, + {file = "wrapt-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15c4ede3fde08723cabab0a892d4b75d35b5a6f0a51c84e6542ac0bdc0507aa"}, + {file = "wrapt-2.4.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e32c5951c36fed88b6c603dc0bab209a62dc3217bd8762413634725fe28f2f3c"}, + {file = "wrapt-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:898513db90d55a4ed3009312d41c12932b8163edbae3535280046d47aaff774f"}, + {file = "wrapt-2.4.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:42d01574bd4bcafc3476e77a95c6c0dd6167101991401325fc5591e2db21a91d"}, + {file = "wrapt-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:68a403adbeb4dd2654d6d108e43e69f0f90e6d34cb7588e0e6589109f6985e67"}, + {file = "wrapt-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:7cb3035b332bc9d21478600ba82c7c71e2d074ed9b4e76364297f54598792227"}, + {file = "wrapt-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:707d2bef68deddd0fc74a81103d91b286a69ac5a9ff0f0a6dad66f8787e86697"}, + {file = "wrapt-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28cb1c2713b4377bf03ddcb3e76d8216e4bb334199066c6122be7eed2f72de8c"}, + {file = "wrapt-2.4.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:96a5023fa63ca2f095f7776c8bfaa1694547577f762646c375abafd8f8ae649b"}, + {file = "wrapt-2.4.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b21924949dedc3ac63725e09b9b0a130e771975f03432789344ed3422c46008"}, + {file = "wrapt-2.4.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2f725af353bb3319c528ee69dbff838599426974288b281a3258d5397b18cb63"}, + {file = "wrapt-2.4.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20327e162ef7953fae56b31a43ca457f94ed1fc7a206d01e3d5c8412d1b91572"}, + {file = "wrapt-2.4.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2cedb743dbdfb9b6d4f11acd8cb6329460264429777e81ea2e86b1b72ea503af"}, + {file = "wrapt-2.4.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5a54744b1193505f19016194979b773ee3754a199e9ad90db18f2e9a18fffa16"}, + {file = "wrapt-2.4.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:b0d38d9cc23e9781e6584f303e5c5a9f0d45b85de92ad45678c4dde8d49d9535"}, + {file = "wrapt-2.4.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a524ca32f0bcdde2b728d9f81f9527d4dd24b13f15d180f05d08cdc01d1cebe0"}, + {file = "wrapt-2.4.1-cp315-cp315-win32.whl", hash = "sha256:47c267617551e906de72f6e7265aa3bce84c63d44513d2d2e735e943422aa0a2"}, + {file = "wrapt-2.4.1-cp315-cp315-win_amd64.whl", hash = "sha256:ee437bd7fd050823ef731aad20e968ca8fe670b95e1841f5d201bfdbad4a4e96"}, + {file = "wrapt-2.4.1-cp315-cp315-win_arm64.whl", hash = "sha256:ebf3b703752b53366fd02b7fbd8c447428e0349c9af3fc17c7a05076dbdd749a"}, + {file = "wrapt-2.4.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:0f5990f5db090f069fcd577cc61cc8d463db73cde132abba9033b0a264fc06d0"}, + {file = "wrapt-2.4.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef19a2590b195ac294deff8ee350a027a479afdd2ef2170ce3900af92406e110"}, + {file = "wrapt-2.4.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:432f402f9b6014403cacf9fd18a6bf089c77964330eae951a155131ab0414f5d"}, + {file = "wrapt-2.4.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb7c0f8bdd21e954bad89d554cfb7431c2f8179842853f199841ab90cbebe914"}, + {file = "wrapt-2.4.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e82a1669d63b79a2b53041bbb6ea096b5763cab3ca698ed7ac244b3acb7cd51"}, + {file = "wrapt-2.4.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b19e71c914c435d2c5652caea386c9bf2807979e957183f5bb06d5ed5fa8b55e"}, + {file = "wrapt-2.4.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a1e4870d3368c6c918f38e308d5dcea970ea5b908ce889c21412f3a517ebf0c3"}, + {file = "wrapt-2.4.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2286e8e4a937966706463d1bcea30dfefee529e6e73e097a18e9e066a07ae6c2"}, + {file = "wrapt-2.4.1-cp315-cp315t-win32.whl", hash = "sha256:80afa3b7010e82899044a2a189c468a0cd8c89980398a59ba3b96b451a6dc5e9"}, + {file = "wrapt-2.4.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3593b43fabab6b59fe77e38f7e40974aae120c30cea3fd1ceaea6621ee96be00"}, + {file = "wrapt-2.4.1-cp315-cp315t-win_arm64.whl", hash = "sha256:ac1939ccf3e1c33f463706fbf52db5075f5eefb6042bcc1f9cf48c2c20ef478c"}, + {file = "wrapt-2.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c2ab24ed5d3a19f31fb815419a61b27904b5b0222eaacac5e1853d5361035df8"}, + {file = "wrapt-2.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69fa244ddc1f9c3e8d390e169ebbf018eee53c2e05bda61c980418df89585ccf"}, + {file = "wrapt-2.4.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:355e766371657ceca791041b82a7943aa3c8a0d879359c112e63bf3c1c8cfea7"}, + {file = "wrapt-2.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52e474424ea1863199e8635a3915b279164929e71b81af97f2105cab1ce78ef2"}, + {file = "wrapt-2.4.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e59f1d5db7bba65f57a2b121eea839214465e878d957d9ab29ad039279ec2a66"}, + {file = "wrapt-2.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5b347c2184906daedae9a2f9fac67cf02c61f7b6498f09cafdcdd6b404ea9be"}, + {file = "wrapt-2.4.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c89bdfe72c2b96bbe431764a90a25723570a896b605d08e08bca6fe150b8a33"}, + {file = "wrapt-2.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4935ad7324d8637715f7cefc35ad65be80900f91d1631947b01bf70651daa328"}, + {file = "wrapt-2.4.1-cp39-cp39-win32.whl", hash = "sha256:c57abea6aa7de8c584b32958d8f606a08d659d3a5a1296ba2038ea9c185a417e"}, + {file = "wrapt-2.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:7033c3c5eb7cd301d9bea8a01660fbe9561c9a3a069d1733e9a712917def0aa2"}, + {file = "wrapt-2.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:730195aa58afaa7dbb5d351e8728cec6763be2605ef82eec9f134950e3c8618e"}, + {file = "wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c"}, + {file = "wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16"}, +] + +[package.extras] +dev = ["pytest", "setuptools"] + +[[package]] +name = "wsproto" +version = "1.3.2" +description = "Pure-Python WebSocket protocol implementation" +optional = false +python-versions = ">=3.10" +files = [ + {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, + {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, +] + +[package.dependencies] +h11 = ">=0.16.0,<1" + +[[package]] +name = "xlrd" +version = "2.0.2" +description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9"}, + {file = "xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9"}, +] + +[package.extras] +build = ["twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +description = "A Python module for creating Excel XLSX files." +optional = false +python-versions = ">=3.8" +files = [ + {file = "xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3"}, + {file = "xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c"}, +] + [[package]] name = "yarl" -version = "1.24.5" +version = "1.25.1" description = "Yet another URL library" optional = false python-versions = ">=3.10" files = [ - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, - {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, - {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, - {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, - {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, - {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, - {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, - {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, - {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, - {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, - {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, - {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, - {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, - {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, - {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95"}, + {file = "yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1"}, + {file = "yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca"}, + {file = "yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426"}, + {file = "yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b"}, + {file = "yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d"}, + {file = "yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2"}, + {file = "yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1"}, + {file = "yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b"}, + {file = "yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e"}, + {file = "yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d"}, + {file = "yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d"}, + {file = "yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68"}, + {file = "yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707"}, + {file = "yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03"}, + {file = "yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26"}, + {file = "yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab"}, + {file = "yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98"}, + {file = "yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa"}, + {file = "yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9"}, + {file = "yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f"}, + {file = "yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57"}, + {file = "yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62"}, + {file = "yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233"}, + {file = "yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b"}, + {file = "yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287"}, + {file = "yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6"}, + {file = "yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3"}, + {file = "yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3"}, + {file = "yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371"}, + {file = "yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc"}, + {file = "yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf"}, + {file = "yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b"}, + {file = "yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c"}, + {file = "yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee"}, + {file = "yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4"}, + {file = "yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225"}, + {file = "yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64"}, + {file = "yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f"}, + {file = "yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd"}, + {file = "yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25"}, + {file = "yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3"}, + {file = "yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7"}, ] [package.dependencies] @@ -4377,4 +8044,4 @@ type = ["pytest-mypy (>=1.0.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.14" -content-hash = "885f769ac40c22e1d6298ee073d3383bd706df862f4c56d1d7300200c55c2040" +content-hash = "3361922bf6a698b1b3c447b0e207645a37b9ef6cff0983b7f324b9c421e8357c" diff --git a/pyproject.toml b/pyproject.toml index d219a2b..d37015e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codespy-ai" -version = "1.2.4" +version = "2.0.0" description = "Code review agent powered by DSPy" readme = "README.md" license = "MIT" @@ -47,12 +47,13 @@ tree-sitter-kotlin = ">=1.0" tree-sitter-objc = ">=3.0" tree-sitter-rust = ">=0.23" tree-sitter-hcl = ">=1.2.0" -mcp = ">=1.29.0,<2.0.0" +mcp = ">=2.2,<3" psycopg = {version = ">=3.1", extras = ["binary", "pool"]} beautifulsoup4 = ">=4.12.0" markdownify = ">=0.15.0" ddgs = ">=8.0.0" pg0-embedded = ">=0.15" +hindsight-api-slim = ">=0.10.1" [tool.poetry.group.dev.dependencies] pytest = ">=8.0.0" diff --git a/src/codespy/__init__.py b/src/codespy/__init__.py index a2b6989..b783fe4 100644 --- a/src/codespy/__init__.py +++ b/src/codespy/__init__.py @@ -1,3 +1,3 @@ """codespy - Code review agent powered by DSPy.""" -__version__ = "1.2.4" +__version__ = "2.0.0" diff --git a/src/codespy/agents/context_safe.py b/src/codespy/agents/context_safe.py index 8bf0209..0e5b720 100644 --- a/src/codespy/agents/context_safe.py +++ b/src/codespy/agents/context_safe.py @@ -60,7 +60,7 @@ class ContextSafe(dspy.Module): - Try/catch: if the inner module raises a context overflow error (unknown models where pre-flight can't estimate), catches it and retries with RLM. - Transparent to inject_context_memory — delegates .signature to inner module. + Transparent to signature injection — delegates .signature to inner module. """ def __init__( @@ -186,7 +186,7 @@ def _should_use_rlm(self, kwargs: dict) -> tuple[bool, str]: return False, "" def _get_current_signature(self): - """Get current signature (may include context_memory if inject_context_memory was called).""" + """Get current signature.""" sig = getattr(self.module, "signature", None) if sig is not None: return sig diff --git a/src/codespy/agents/cost_tracker.py b/src/codespy/agents/cost_tracker.py index 1be9f31..133194c 100644 --- a/src/codespy/agents/cost_tracker.py +++ b/src/codespy/agents/cost_tracker.py @@ -8,13 +8,15 @@ import sys import threading import time -from contextlib import AbstractContextManager from dataclasses import dataclass from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING, Any import dspy # type: ignore[import-untyped] +if TYPE_CHECKING: + from codespy.agents.dspy_config import LMScope + logger = logging.getLogger(__name__) @@ -28,14 +30,26 @@ class SignatureStats: call_count: int = 0 start_time: float | None = None end_time: float | None = None + input_tokens: int = 0 + output_tokens: int = 0 + input_cost: float = 0.0 + output_cost: float = 0.0 + external_duration_seconds: float = 0.0 @property def duration_seconds(self) -> float: - """Get duration in seconds, or 0 if not completed.""" - if self.start_time is None: - return 0.0 - end = self.end_time if self.end_time is not None else time.time() - return end - self.start_time + """Get duration in seconds, or 0 if not completed. + + For signatures tracked via SignatureContext, this is wall-clock time + from start to end. For external calls (e.g., cerebral), this is the + sum of per-call work durations (not wall-clock), consistent with how + cost/tokens are summed. + """ + wall = 0.0 + if self.start_time is not None: + end = self.end_time if self.end_time is not None else time.time() + wall = end - self.start_time + return wall + self.external_duration_seconds def to_dict(self) -> dict: """Convert to dictionary for serialization.""" @@ -45,6 +59,11 @@ def to_dict(self) -> dict: "tokens": self.tokens, "call_count": self.call_count, "duration_seconds": self.duration_seconds, + "input_tokens": self.input_tokens, + "output_tokens": self.output_tokens, + "input_cost": self.input_cost, + "output_cost": self.output_cost, + "external_duration_seconds": self.external_duration_seconds, } @@ -77,7 +96,17 @@ def start_signature(self, signature_name: str) -> None: self._signature_stats[signature_name].start_time = time.time() self._signature_stats[signature_name].end_time = None - def end_signature(self, signature_name: str, cost: float, tokens: int, call_count: int) -> None: + def end_signature( + self, + signature_name: str, + cost: float, + tokens: int, + call_count: int, + input_tokens: int = 0, + output_tokens: int = 0, + input_cost: float = 0.0, + output_cost: float = 0.0, + ) -> None: """Mark the end of a signature's execution with its costs. Args: @@ -85,6 +114,10 @@ def end_signature(self, signature_name: str, cost: float, tokens: int, call_coun cost: Total cost for this signature's LLM calls tokens: Total tokens used by this signature call_count: Number of LLM calls made by this signature + input_tokens: Input/prompt tokens used + output_tokens: Output/completion tokens used + input_cost: Cost for input tokens + output_cost: Cost for output tokens """ with self._lock: if signature_name not in self._signature_stats: @@ -94,6 +127,52 @@ def end_signature(self, signature_name: str, cost: float, tokens: int, call_coun stats.cost += cost stats.tokens += tokens stats.call_count += call_count + stats.input_tokens += input_tokens + stats.output_tokens += output_tokens + stats.input_cost += input_cost + stats.output_cost += output_cost + + def add_external_call( + self, + name: str, + cost: float, + tokens: int, + calls: int = 1, + input_tokens: int = 0, + output_tokens: int = 0, + input_cost: float = 0.0, + output_cost: float = 0.0, + duration: float = 0.0, + ) -> None: + """Accumulate costs from external LLM calls (not tracked by DSPy). + + Creates a SignatureStats entry if missing, without touching + start_time/end_time. Duration is summed per-call work time for the + bucket (not wall-clock), consistent with how cost/tokens are summed. + + Args: + name: Bucket name for the external call (e.g., "memory_retain") + cost: Cost in USD for the call(s) + tokens: Total tokens used + calls: Number of calls (default 1) + input_tokens: Input/prompt tokens used + output_tokens: Output/completion tokens used + input_cost: Cost for input tokens + output_cost: Cost for output tokens + duration: Duration in seconds for this call (default 0.0) + """ + with self._lock: + if name not in self._signature_stats: + self._signature_stats[name] = SignatureStats(name=name) + stats = self._signature_stats[name] + stats.cost += cost + stats.tokens += tokens + stats.call_count += calls + stats.input_tokens += input_tokens + stats.output_tokens += output_tokens + stats.input_cost += input_cost + stats.output_cost += output_cost + stats.external_duration_seconds += duration @property def total_cost(self) -> float: @@ -141,19 +220,28 @@ def get_all_signature_stats(self) -> dict[str, SignatureStats]: call_count=v.call_count, start_time=v.start_time, end_time=v.end_time, + input_tokens=v.input_tokens, + output_tokens=v.output_tokens, + input_cost=v.input_cost, + output_cost=v.output_cost, + external_duration_seconds=v.external_duration_seconds, ) for k, v in self._signature_stats.items() } -def _get_history_entries() -> list[dict]: +def _get_history_entries(lm: Any | None = None) -> list[dict]: """Get current LM history entries from DSPy. + Args: + lm: Optional LM to read history from. If None, uses dspy.settings.lm. + Returns: List of history entries, or empty list if LM not configured """ try: - lm = dspy.settings.lm + if lm is None: + lm = dspy.settings.lm if lm is not None and hasattr(lm, "history"): return lm.history except Exception: @@ -161,13 +249,16 @@ def _get_history_entries() -> list[dict]: return [] -def _get_history_uuids() -> set[str]: +def _get_history_uuids(lm: Any | None = None) -> set[str]: """Get UUIDs of current history entries. + Args: + lm: Optional LM to read history from. If None, uses dspy.settings.lm. + Returns: Set of UUIDs from current history """ - entries = _get_history_entries() + entries = _get_history_entries(lm) return {entry.get("uuid", "") for entry in entries if entry.get("uuid")} @@ -196,7 +287,7 @@ def _as_number(value: object) -> float: def _calculate_costs_from_entries( entries: list[dict], exclude_uuids: set[str] -) -> tuple[float, int, int]: +) -> tuple[float, int, int, int, int, float, float]: """Calculate costs from history entries, excluding specific UUIDs. Every field is read defensively: cost accounting is observability, so a @@ -208,11 +299,16 @@ def _calculate_costs_from_entries( exclude_uuids: Set of UUIDs to exclude from calculation Returns: - Tuple of (total_cost, total_tokens, call_count) + Tuple of (total_cost, total_tokens, call_count, input_tokens, output_tokens, + input_cost, output_cost) """ total_cost = 0.0 total_tokens = 0 call_count = 0 + input_tokens = 0 + output_tokens = 0 + input_cost = 0.0 + output_cost = 0.0 for entry in entries: if not isinstance(entry, dict): @@ -220,17 +316,121 @@ def _calculate_costs_from_entries( entry_uuid = entry.get("uuid", "") if entry_uuid and entry_uuid not in exclude_uuids: - total_cost += _as_number(entry.get("cost")) + # Skip DSPy cache hits (not billed) + response = entry.get("response") + if response and response.get("cache_hit"): + continue + + entry_cost = _as_number(entry.get("cost")) + total_cost += entry_cost # Get tokens from usage usage = entry.get("usage") + entry_input_tokens = 0 + entry_output_tokens = 0 if isinstance(usage, dict): - total_tokens += int(_as_number(usage.get("prompt_tokens"))) - total_tokens += int(_as_number(usage.get("completion_tokens"))) + entry_input_tokens = int(_as_number(usage.get("prompt_tokens"))) + entry_output_tokens = int(_as_number(usage.get("completion_tokens"))) + input_tokens += entry_input_tokens + output_tokens += entry_output_tokens + total_tokens += entry_input_tokens + entry_output_tokens + + # Calculate split costs using litellm if model is available + entry_input_cost, entry_output_cost = _calculate_split_cost( + entry, entry_input_tokens, entry_output_tokens, entry_cost + ) + input_cost += entry_input_cost + output_cost += entry_output_cost call_count += 1 - return total_cost, total_tokens, call_count + return ( + total_cost, + total_tokens, + call_count, + input_tokens, + output_tokens, + input_cost, + output_cost, + ) + + +def _calculate_split_cost( + entry: dict, prompt_tokens: int, completion_tokens: int, fallback_cost: float +) -> tuple[float, float]: + """Calculate split input/output cost for a history entry. + + Uses litellm.cost_per_token if model is available. When entry["cost"] is + a number, we compute output_cost from list price and input_cost = cost - + output_cost (so row sums to Total). When cost is None, we use the list + split for both and the row cost equals the list split. + + Args: + entry: History entry dict + prompt_tokens: Number of prompt tokens + completion_tokens: Number of completion tokens + fallback_cost: Fallback total cost for proportional split + + Returns: + Tuple of (input_cost, output_cost) + """ + model = entry.get("model") + if not model: + # Fall back to proportional split by token ratio + return _proportional_split(prompt_tokens, completion_tokens, fallback_cost) + + try: + import litellm + + prompt_cost, completion_cost = litellm.cost_per_token( + model=model, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + ) + list_input_cost = float(prompt_cost) + list_output_cost = float(completion_cost) + list_total = list_input_cost + list_output_cost + + # Check if we have a billed cost (could be 0.0 which is falsy, so check for None explicitly) + billed_cost = entry.get("cost") + if billed_cost is None: + # No billed cost: use list split (row will sum to list_total) + return list_input_cost, list_output_cost + + # Have billed cost: output_cost is list price, input_cost = billed - output + billed = float(billed_cost) + # If list_total is 0 (rare), use proportional split + if list_total == 0: + return _proportional_split(prompt_tokens, completion_tokens, billed) + + # Cap output_cost at billed (don't let input_cost go negative) + output_cost = min(list_output_cost, billed) + input_cost = billed - output_cost + return input_cost, output_cost + + except Exception: + # Model may not have pricing or be available; use proportional split + return _proportional_split(prompt_tokens, completion_tokens, fallback_cost) + + +def _proportional_split( + input_tokens: int, output_tokens: int, total_cost: float +) -> tuple[float, float]: + """Split cost proportionally by token count. + + Args: + input_tokens: Number of input tokens + output_tokens: Number of output tokens + total_cost: Total cost to split + + Returns: + Tuple of (input_cost, output_cost) + """ + total = input_tokens + output_tokens + if total == 0: + return 0.0, 0.0 + input_ratio = input_tokens / total + return total_cost * input_ratio, total_cost * (1 - input_ratio) class SignatureContext: @@ -239,8 +439,8 @@ class SignatureContext: Two responsibilities, both keyed off the same name: 1. **LM selection** — applies the model, temperature, and reasoning effort - configured for this name (``signatures.`` or ``memory.``), - falling back to the top-level defaults. + configured for this name (``review.`` or ``memory.hippocampus.`` + for ``memory_``), falling back to the top-level defaults. 2. **Cost attribution** — uses DSPy's LM history to attribute costs reliably, even during parallel execution with dspy.Parallel, by recording history UUIDs on entry and summing only the new entries. @@ -261,9 +461,10 @@ def __init__(self, signature_name: str, tracker: "CostTracker") -> None: self.signature_name = signature_name self.tracker = tracker self._before_uuids: set[str] = set() + self._before_extraction_uuids: set[str] = set() # Annotated so the None default doesn't narrow the attribute to # ``None``, which would hide the enter/exit calls from type checking. - self._lm_context: AbstractContextManager[Any] | None = None + self._lm_scope: LMScope | None = None def __enter__(self) -> "SignatureContext": """Enter the context, applying the LM and capturing history state. @@ -279,14 +480,16 @@ def __enter__(self) -> "SignatureContext": # Apply this name's LM first, so the history we snapshot below belongs # to the LM that will actually serve the enclosed calls. - self._lm_context = lm_context(self.signature_name) - self._lm_context.__enter__() + self._lm_scope = lm_context(self.signature_name) + self._lm_scope.__enter__() try: - self._before_uuids = _get_history_uuids() + # Snapshot UUIDs from both main LM and extraction LM + self._before_uuids = _get_history_uuids(self._lm_scope.lm) + self._before_extraction_uuids = _get_history_uuids(self._lm_scope.extraction_lm) self.tracker.start_signature(self.signature_name) except BaseException: - self._lm_context.__exit__(*sys.exc_info()) - self._lm_context = None + self._lm_scope.__exit__(*sys.exc_info()) + self._lm_scope = None raise return self @@ -309,17 +512,43 @@ def __exit__( instead of quiet and global. """ try: - # Read history before leaving the LM context, so dspy.settings.lm - # still points at the LM whose history we need. - entries = _get_history_entries() - cost, tokens, call_count = _calculate_costs_from_entries(entries, self._before_uuids) - self.tracker.end_signature(self.signature_name, cost, tokens, call_count) + # Read history from both LMs before leaving the context + main_entries = _get_history_entries(self._lm_scope.lm if self._lm_scope else None) + extraction_entries = _get_history_entries( + self._lm_scope.extraction_lm if self._lm_scope else None + ) + + # Calculate costs from both histories + main_result = _calculate_costs_from_entries(main_entries, self._before_uuids) + extraction_result = _calculate_costs_from_entries( + extraction_entries, self._before_extraction_uuids + ) + + # Combine results: sum all numeric fields + cost = main_result[0] + extraction_result[0] + tokens = main_result[1] + extraction_result[1] + call_count = main_result[2] + extraction_result[2] + input_tokens = main_result[3] + extraction_result[3] + output_tokens = main_result[4] + extraction_result[4] + input_cost = main_result[5] + extraction_result[5] + output_cost = main_result[6] + extraction_result[6] + + self.tracker.end_signature( + self.signature_name, + cost=cost, + tokens=tokens, + call_count=call_count, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_cost=input_cost, + output_cost=output_cost, + ) except Exception as e: logger.warning("Cost calculation failed for %s: %s", self.signature_name, e) finally: - if self._lm_context is not None: - self._lm_context.__exit__(exc_type, exc_val, exc_tb) - self._lm_context = None + if self._lm_scope is not None: + self._lm_scope.__exit__(exc_type, exc_val, exc_tb) + self._lm_scope = None async def __aenter__(self) -> "SignatureContext": """Async enter the context.""" diff --git a/src/codespy/agents/dspy_config.py b/src/codespy/agents/dspy_config.py index 4a8c658..dbdc2a9 100644 --- a/src/codespy/agents/dspy_config.py +++ b/src/codespy/agents/dspy_config.py @@ -1,18 +1,100 @@ """DSPy and LiteLLM configuration utilities.""" import logging +from dataclasses import dataclass +from types import TracebackType +from typing import Any import dspy # type: ignore[import-untyped] import litellm # type: ignore[import-untyped] from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped] from codespy.config import Settings, get_settings -from codespy.config_memory import REFLECTION_MODULES, LLMSettings +from codespy.config_memory import MEMORY_CONSOLIDATION, MEMORY_MENTAL_MODELS, MEMORY_RECALL, MEMORY_RETAIN, REFLECTION_MODULES, LLMSettings from codespy.config_utils import secret_value logger = logging.getLogger(__name__) +@dataclass +class LMScope: + """Container for LM context with both main and extraction LMs. + + Returned by lm_context() to expose both the main LM (used by DSPy predictors) + and the extraction LM (used by TwoStepAdapter) for cost attribution. + The ctx is the actual dspy.context that must be exited to release resources. + + Attributes: + ctx: The dspy.context manager (must call __exit__ to release). + lm: The main LM used for reasoning/generation. + extraction_lm: The extraction LM used for structured field extraction. + """ + + ctx: Any + lm: Any + extraction_lm: Any + + def __enter__(self) -> "LMScope": + """Enter the context.""" + self.ctx.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the context, releasing the LM.""" + self.ctx.__exit__(exc_type, exc_val, exc_tb) + + async def __aenter__(self) -> "LMScope": + """Async enter the context.""" + return self.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Async exit the context.""" + self.__exit__(exc_type, exc_val, exc_tb) + + +def new_extraction_lm(settings: Settings) -> dspy.LM: + """Build a fresh extraction LM for per-context TwoStepAdapter use. + + The extraction LM is used by TwoStepAdapter for the second stage: + deterministic field extraction from the main LM's free-form response. + A fresh instance per context ensures correct cost attribution and + thread isolation. + + The extraction model is taken from the "default" LLMSettings via + ``settings.get_llm_config("default")``, using its ``extraction_model`` + (which falls back to ``default_model``). This matches the global fallback + in ``configure_dspy()`` and does NOT use per-signature overrides. + + Args: + settings: Application settings. + + Returns: + A configured dspy.LM with temperature=0.0 and no reasoning. + """ + defaults = settings.get_llm_config("default") + extraction_model = defaults.extraction_model + return new_lm( + settings, + defaults.model_copy( + update={ + "model": extraction_model, + "reasoning_effort": None, + "temperature": 0.0, + } + ), + ) + + def _resolve_max_tokens(model: str, max_tokens: int) -> int: """Clamp a configured output budget to the model's real output ceiling. @@ -95,25 +177,25 @@ def new_lm(settings: Settings, config: LLMSettings) -> dspy.LM: "model": config.model, "temperature": config.temperature, "max_tokens": _resolve_max_tokens(config.model, config.max_tokens), - "timeout": settings.llm_timeout, - "num_retries": settings.llm_retries, + "timeout": settings.llm.timeout, + "num_retries": settings.llm.retries, "drop_params": True, "reasoning_effort": config.reasoning_effort, } # Cache system prompts via explicit Anthropic-style cache_control markers. # Only injected for providers that use explicit markers (Anthropic, Bedrock # Anthropic); OpenAI/Gemini have automatic caching that needs no markers. - if settings.enable_prompt_caching and _supports_cache_control(config.model): + if _supports_cache_control(config.model): lm_kwargs["cache_control_injection_points"] = [{"location": "message", "role": "system"}] - elif settings.enable_prompt_caching: + else: logger.debug( - f"Prompt caching enabled but model {config.model} does not use " - f"explicit cache_control markers — skipping injection" + f"Model {config.model} does not use explicit cache_control markers — " + f"using provider-automatic caching" ) return dspy.LM(**lm_kwargs) -def lm_context(name: str): +def lm_context(name: str) -> LMScope: """Return a ``dspy.context`` applying the LM configured for ``name``. ``name`` is a signature or reflection module name — see @@ -121,16 +203,21 @@ def lm_context(name: str): named unit of work runs on its own configured model, falling back to the top-level defaults when it declares no overrides. + Also creates a per-context extraction LM so that TwoStepAdapter's extraction + calls can be attributed correctly to the calling signature. + Args: name: The signature or reflection module name. Returns: - A context manager that scopes the LM to the enclosed block. + An LMScope wrapping the context manager, main LM, and extraction LM. """ settings = get_settings() llm_config = settings.get_llm_config(name) lm = new_lm(settings, llm_config) - return dspy.context(lm=lm) + extraction_lm = new_extraction_lm(settings) + ctx = dspy.context(lm=lm, adapter=TwoStepAdapter(extraction_lm)) + return LMScope(ctx=ctx, lm=lm, extraction_lm=extraction_lm) def configure_dspy(settings: Settings) -> None: @@ -150,24 +237,24 @@ def configure_dspy(settings: Settings) -> None: Args: settings: Application settings containing model and API key configuration. """ - model = settings.default_model + model = settings.llm.default_model # Configure LiteLLM environment if needed - openai_key = secret_value(settings.openai_api_key) + openai_key = secret_value(settings.llm.openai_api_key) if openai_key: litellm.openai_key = openai_key - anthropic_key = secret_value(settings.anthropic_api_key) + anthropic_key = secret_value(settings.llm.anthropic_api_key) if anthropic_key: litellm.anthropic_key = anthropic_key # Set up AWS credentials for Bedrock if using Bedrock model if model.startswith("bedrock/"): import os - os.environ["AWS_REGION_NAME"] = settings.aws_region - aws_access = secret_value(settings.aws_access_key_id) + os.environ["AWS_REGION_NAME"] = settings.llm.aws_region + aws_access = secret_value(settings.llm.aws_access_key_id) if aws_access: os.environ["AWS_ACCESS_KEY_ID"] = aws_access - aws_secret = secret_value(settings.aws_secret_access_key) + aws_secret = secret_value(settings.llm.aws_secret_access_key) if aws_secret: os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret @@ -179,17 +266,7 @@ def configure_dspy(settings: Settings) -> None: # Extraction LM for TwoStepAdapter's second stage: deterministic field extraction # from the main LM's free-form response. Never uses reasoning; temperature=0.0 for # deterministic output. - extraction_model = defaults.extraction_model - extraction_lm = new_lm( - settings, - defaults.model_copy( - update={ - "model": extraction_model, - "reasoning_effort": None, - "temperature": 0.0, - } - ), - ) + extraction_lm = new_extraction_lm(settings) dspy.settings.configure( lm=lm, @@ -201,17 +278,16 @@ def configure_dspy(settings: Settings) -> None: enable_memory_cache=True, enable_disk_cache=False, memory_max_entries=10000 ) - if not settings.enable_prompt_caching: - prompt_cache_status = "disabled" - elif _supports_cache_control(model): + if _supports_cache_control(model): prompt_cache_status = "enabled (cache_control markers)" else: prompt_cache_status = "enabled (provider-automatic, no markers)" + extraction_model = defaults.extraction_model logger.info( f"Configured DSPy with model: {model} " f"(TwoStepAdapter with extraction_model={extraction_model}, " f"max_tokens={_resolve_max_tokens(defaults.model, defaults.max_tokens)}, " - f"timeout={settings.llm_timeout}s, retries={settings.llm_retries}, " + f"timeout={settings.llm.timeout}s, retries={settings.llm.retries}, " f"provider prompt caching {prompt_cache_status})" ) @@ -220,7 +296,7 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: """Verify that all configured models are accessible. Checks the default model, all per-signature model overrides, and the - memory reflection models, so a typo in any of them fails fast at startup + memory reflection modules, so a typo in any of them fails fast at startup rather than mid-review. Args: @@ -230,21 +306,38 @@ def verify_model_access(settings: Settings) -> tuple[bool, str]: Tuple of (success, message) """ # Collect all unique models from config - models_to_check: set[str] = {settings.default_model} + models_to_check: set[str] = {settings.llm.default_model} # Check all signature-specific models - for _sig_name, sig_config in settings.signatures.items(): + for _sig_name, sig_config in settings.review.signatures().items(): if sig_config.model: models_to_check.add(sig_config.model) # Global extraction model (if different from default_model) - if settings.extraction_model: - models_to_check.add(settings.extraction_model) + if settings.llm.extraction_model: + models_to_check.add(settings.llm.extraction_model) + # Memory reflection modules for module in REFLECTION_MODULES: reflection = settings.get_llm_config(module) models_to_check.add(reflection.model) + # Cerebral retain model + cerebral = settings.get_llm_config(MEMORY_RETAIN) + models_to_check.add(cerebral.model) + + # Cerebral consolidation model + consolidation = settings.get_llm_config(MEMORY_CONSOLIDATION) + models_to_check.add(consolidation.model) + + # Prefrontal reflect model (only when reflects > 0, since with reflects=0 no LLM runs) + if settings.memory.prefrontal.reflects > 0: + prefrontal = settings.get_llm_config(MEMORY_RECALL) + models_to_check.add(prefrontal.model) + # Mental-models refresh model (only when reflects > 0) + mental_models = settings.get_llm_config(MEMORY_MENTAL_MODELS) + models_to_check.add(mental_models.model) + # Check each model verified: list[str] = [] failed: list[str] = [] @@ -281,16 +374,5 @@ def filter(self, record: logging.LogRecord) -> bool: return not ("Task was destroyed" in msg and "LoggingWorker" in msg) -class _MCPRequestFilter(logging.Filter): - """Filter to suppress all noisy 'Processing request of type' MCP server messages.""" - - def filter(self, record: logging.LogRecord) -> bool: - return "Processing request of type" not in record.getMessage() - - # Suppress LiteLLM's async logging worker warnings that occur during multi-threaded execution logging.getLogger("asyncio").addFilter(_TaskDestroyedFilter()) - -# Suppress noisy MCP server "Processing request" messages -logging.getLogger("mcp.server").addFilter(_MCPRequestFilter()) -logging.getLogger("mcp.server.lowlevel").addFilter(_MCPRequestFilter()) diff --git a/src/codespy/agents/memory/cerebral/__init__.py b/src/codespy/agents/memory/cerebral/__init__.py new file mode 100644 index 0000000..aa18260 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/__init__.py @@ -0,0 +1,3 @@ +from codespy.agents.memory.cerebral.cerebral import Cerebral + +__all__ = ["Cerebral"] diff --git a/src/codespy/agents/memory/cerebral/cerebral.py b/src/codespy/agents/memory/cerebral/cerebral.py new file mode 100644 index 0000000..9b28c03 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/cerebral.py @@ -0,0 +1,1223 @@ +"""Cerebral – Hindsight semantic memory for episodes. + +Uses MemoryEngine (direct Python API) sharing the same PostgreSQL +instance as the episodic store, under the ``semantic`` schema. + +Write side: ``retain_episode`` retains Hippocampus episodes, tagged with +``repo:``/``org:``. Scopes are recorded per run_id for later consolidation. +``consolidate_run`` submits one consolidation after all episode saves complete, +which refreshes mental models ("briefings") once per touched scope. + +Read side (used by Prefrontal only): ``arecall``/``recall``, +``areflect``/``reflect``, ``aget_mental_models`` and +``aget_observation_history``. Cerebral never reads Prefrontal output. +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import threading +import time +from collections import defaultdict +from collections.abc import Sequence +from dataclasses import dataclass, field +from datetime import datetime +from typing import TYPE_CHECKING, Any + +# Module constants (must be defined before the Hindsight import) +HINDSIGHT_SCHEMA_DEFAULT = "semantic" + +# Hindsight defaults embeddings_provider / reranker_provider to "local", which +# needs sentence-transformers and emits a misleading startup warning +# (hindsight_api/config.py _validate). That config is built and cached at IMPORT +# time — the `from hindsight_api import MemoryEngine` below pulls in +# engine.llm_wrapper, whose module-level _get_raw_config() calls +# HindsightConfig.from_env(). So these env vars MUST be set before that import +# line, not in Cerebral.__init__ (too late). We inject our own LiteLLM-SDK +# embeddings and an RRF-passthrough cross-encoder in Cerebral.__init__, so the +# env-driven providers are never used; align them to non-local values so the +# warning does not fire. setdefault keeps operator overrides intact. +os.environ.setdefault("HINDSIGHT_API_EMBEDDINGS_PROVIDER", "litellm-sdk") +os.environ.setdefault("HINDSIGHT_API_RERANKER_PROVIDER", "none") + +# Upstream #2638: The maintenance routines (mental_models_with_cron, etc.) are +# installed by migrations into `database_schema` and called by name from that +# schema via fq_routine(). Cerebral sets its schema only via the tenant +# extension, so without this env var, `database_schema` defaults to "public". +# With database_schema="public" and migrations running with target_schema="semantic", +# the routines are never created. Setting this makes the routines install into +# "semantic" and be called from "semantic", matching the tenant schema. +os.environ.setdefault("HINDSIGHT_API_DATABASE_SCHEMA", HINDSIGHT_SCHEMA_DEFAULT) + +from hindsight_api import MemoryEngine +from hindsight_api.engine.cross_encoder import RRFPassthroughCrossEncoder +from hindsight_api.engine.embeddings import LiteLLMSDKEmbeddings +from hindsight_api.engine.memory_engine import Budget +from hindsight_api.engine.task_backend import SyncTaskBackend +from hindsight_api.extensions import OperationValidationError +from hindsight_api.extensions.builtin.tenant import DefaultTenantExtension +from hindsight_api.models import RequestContext +from hindsight_api.worker.exceptions import DeferOperation + +# Install delta operations coercion shim for string-encoded operations +# (see delta_ops_compat.py for details) +from codespy.agents.memory.cerebral.delta_ops_compat import install_delta_ops_coercion + +install_delta_ops_coercion() + +# Install native structured output support and Bedrock schema sanitizer +from codespy.agents.memory.cerebral.structured_output import ( + install_bedrock_schema_sanitizer, + register_native_structured_output_overrides, +) + +register_native_structured_output_overrides() +install_bedrock_schema_sanitizer() + + +class _SchemaSyncTaskBackend(SyncTaskBackend): + """SyncTaskBackend that tags inline tasks with Cerebral's schema. + + Hindsight only sets ``_schema`` in the worker poller. Without it, + ``_execute_task`` passes ``schema=None`` to the consolidation webhook, + which then queries bare ``webhooks`` (search_path -> public) and fails with + 'relation "webhooks" does not exist'. + + The root cause upstream is ``fq_table_explicit(table, None)`` returning + a bare table name instead of falling back to ``database_schema`` like + ``fq_table`` and ``fq_routine`` do. This subclass can be removed once + Hindsight falls back to ``database_schema`` there. + + This backend also stands in for the worker poller's fail-marking: when a + task fails, it marks the operation row as failed so that deduplication + no longer blocks subsequent submissions. + + Additionally, this backend sets a ContextVar (``current_memory_task``) around + task execution to enable cost attribution to ``memory_consolidation`` and + ``memory_mental_models`` buckets. + """ + + def __init__(self, schema: str): + super().__init__() + self._schema = schema + self._engine: MemoryEngine | None = None + + def attach(self, engine: MemoryEngine) -> None: + """Attach a MemoryEngine for DB operations on task failure.""" + self._engine = engine + + async def submit_task(self, task_dict): + # Work on a copy to avoid mutating the caller's dict + task = dict(task_dict) + if "_schema" not in task: + task["_schema"] = self._schema + + # Set contextvar for cost attribution when running consolidation or refresh + # Nested tasks: the innermost task wins; outer tag restored in finally + task_type = task.get("type") + token = None + if task_type in ("consolidation", "refresh_mental_model"): + from codespy.agents.memory.cerebral.cost import current_memory_task + + token = current_memory_task.set(task_type) + + try: + await super().submit_task(task) + except DeferOperation as exc: + # Handle deferral: mark refresh_mental_model as cancelled (not failed) + if task_type == "refresh_mental_model": + await self._mark_cancelled_if_pending(task, exc) + logger.info( + "cerebral: briefing %s refresh deferred until %s", + task.get("mental_model_id", "unknown"), + exc.exec_date.isoformat() if hasattr(exc, "exec_date") else "unknown", + ) + return # Don't re-raise - deferral is expected, not an error + # Other task types: re-raise as normal (fallback to existing behavior) + raise + except Exception as exc: + # Mark the operation as failed if it has an operation_id + await self._mark_failed_if_pending(task, exc) + raise + finally: + if token is not None: + from codespy.agents.memory.cerebral.cost import current_memory_task + + current_memory_task.reset(token) + + async def _mark_failed_if_pending(self, task: dict, exc: Exception) -> None: + """Mark the operation as failed if it's still pending.""" + op_id = task.get("operation_id") + if not op_id or self._engine is None: + return + + from hindsight_api.engine.db_utils import acquire_with_retry + + try: + # Format error message (truncate to 5000 chars) + try: + from hindsight_api.worker.exceptions import format_task_error + + error_msg = format_task_error(exc) + except Exception: + error_msg = f"{type(exc).__name__}: {exc}" + if len(error_msg) > 5000: + error_msg = error_msg[:5000] + + async with acquire_with_retry(self._engine._backend, max_retries=1) as conn: + await conn.execute( + f""" + UPDATE "{self._schema}".async_operations + SET status = 'failed', + error_message = $2, + updated_at = NOW() + WHERE operation_id = $1 + AND status = 'pending' + """, + op_id, + error_msg, + ) + except Exception: + # Log but don't propagate - the original exception is more important + logger.warning( + "Failed to mark operation %s as failed", + op_id, + exc_info=True, + ) + + async def _mark_cancelled_if_pending(self, task: dict, exc: DeferOperation) -> None: + """Mark the operation as cancelled if it's still pending (deferral). + + Deferral from min_refresh_interval_seconds is not a failure - the + operation is effectively skipped and will be retried on the next + consolidation after the interval expires. + """ + op_id = task.get("operation_id") + if not op_id or self._engine is None: + return + + from hindsight_api.engine.db_utils import acquire_with_retry + + try: + exec_date = exc.exec_date.isoformat() if hasattr(exc, "exec_date") else "unknown" + error_msg = f"deferred by min_refresh_interval_seconds until {exec_date}" + + async with acquire_with_retry(self._engine._backend, max_retries=1) as conn: + await conn.execute( + f""" + UPDATE "{self._schema}".async_operations + SET status = 'cancelled', + error_message = $2, + updated_at = NOW() + WHERE operation_id = $1 + AND status = 'pending' + """, + op_id, + error_msg, + ) + except Exception: + # Log but don't propagate - the original deferral is more important + logger.warning( + "Failed to mark operation %s as cancelled", + op_id, + exc_info=True, + ) + +from codespy.agents.memory.cerebral.cost import ( + BUCKET_MEMORY_CONSOLIDATION, + BUCKET_MEMORY_MENTAL_MODELS, + BUCKET_MEMORY_OTHER, + CerebralCostRecorder, + MeteredLiteLLMSDKEmbeddings, + register_cerebral_cost_recorder, +) +from codespy.agents.memory.cerebral.routines import ensure_maintenance_routines +from codespy.agents.memory.prefrontal.reach import ( + TAG_ORG, + TAG_PROJECT_SCOPE, + TAG_REPO, + mental_model_id, + repo_tags, + scope_tags, +) + +if TYPE_CHECKING: + from codespy.agents.memory.hippocampus.episode import Episode + +logger = logging.getLogger(__name__) + + +@dataclass +class ReflectSummary: + """Summary of a reflect call for monitoring and debugging. + + Attributes: + iterations: Number of reflect iterations performed. + llm_calls: Total LLM calls (map calls + reduce + rewrite). + map_calls: Number of map calls in split synthesis (0 when history fits in one chunk). + rewrite: Whether a final rewrite occurred. + tools: List of (tool_name, output_tokens) tuples. Output size is estimated + in tokens (chars/4 heuristic) from the JSON-serialized tool output. + usage: Token usage dict with input_tokens, output_tokens, thoughts_tokens. + empty: Whether the result text is empty. + """ + + iterations: int = 0 + llm_calls: int = 0 + map_calls: int = 0 + rewrite: bool = False + tools: list[tuple[str, int]] = field(default_factory=list) + usage: dict[str, int] = field(default_factory=dict) + empty: bool = True + +HINDSIGHT_SCHEMA = HINDSIGHT_SCHEMA_DEFAULT + +# Static briefing trigger configuration (hardcoded per plan) +# Delta mode with reduced evidence caps to cut cost while preserving quality. +# - mode=delta: Only new facts since last refresh (structured delta-ops call) +# - exclude_mental_models=True: Briefings exclude other briefings +# - include_chunks=False: Observation text only (no file chunks) +# - reflect_search_observations_max_tokens=3000: Down from 5000 default +# - reflect_search_observations_include_entities=False: Exclude entity metadata +_BRIEFING_TRIGGER_STATIC = { + "mode": "delta", + "exclude_mental_models": True, + "include_chunks": False, + "reflect_search_observations_max_tokens": 3000, + "reflect_search_observations_include_entities": False, +} + +# Content Hindsight's HTTP API gives a mental model before its first refresh. +# Prefrontal skips briefings that still hold it. +MENTAL_MODEL_PLACEHOLDER = "Generating content..." + +# Generic (not code-review specific) question each briefing answers. +MENTAL_MODEL_SOURCE_QUERY = ( + "What should an agent starting work here know so it does not rediscover it: " + "structure and where things live, conventions, invariants and constants, " + "dependencies and integrations, pitfalls and recurring problems, facts shown to be wrong." +) + +OBSERVATIONS_MISSION = ( + "Merge facts about the same subject into one observation. Keep details that matter only " + "to one kind of work (e.g. documentation, security, dependencies) instead of generalizing " + "them away. When a fact is marked 'supersedes:', update the observation; when marked " + "'RETRACTED', mark the observation invalid." +) + +# Warn if the operator has overridden HINDSIGHT_API_DATABASE_SCHEMA to a different +# value than our schema — the routines would then be missing again (upstream #2638). +if os.environ.get("HINDSIGHT_API_DATABASE_SCHEMA") != HINDSIGHT_SCHEMA: + logger.warning( + "HINDSIGHT_API_DATABASE_SCHEMA is set to %r, which differs from the " + "expected schema %r. Maintenance routines may not be found. " + "See upstream #2638 for details.", + os.environ.get("HINDSIGHT_API_DATABASE_SCHEMA"), + HINDSIGHT_SCHEMA, + ) + + +class Cerebral: + """Retains episode observations and artifacts in Hindsight semantic memory. + + Uses MemoryEngine directly — no HTTP server. All calls are in-process + against the same PostgreSQL instance under the ``semantic`` schema. + """ + + def __init__( + self, + database_url: str, + llm_provider: str, + llm_model: str | None = None, + llm_api_key: str | None = None, + llm_base_url: str | None = None, + bank_id: str = "codebase", + embeddings_model: str = "openai/text-embedding-3-small", + retain_chunk_size: int = 12288, + mental_models: bool = True, + max_mental_model_tokens: int = 2048, + reflect_llm_model: str | None = None, + reflect_llm_api_key: str | None = None, + reflect_llm_base_url: str | None = None, + consolidation_llm_model: str | None = None, + consolidation_llm_api_key: str | None = None, + consolidation_llm_base_url: str | None = None, + extraction_llm_model: str | None = None, + extraction_llm_api_key: str | None = None, + extraction_llm_base_url: str | None = None, + embeddings_max_input_chars: int | None = None, + min_mental_model_refresh_seconds: int = 0, + max_observations_per_scope: int = -1, + ): + # Fail fast: test the embedding model before building MemoryEngine. + # A bad model name, missing creds, or unavailable region surfaces here + # with a clear error instead of buried inside MemoryEngine.initialize(). + import litellm + try: + litellm.embedding(model=embeddings_model, input=["test"], timeout=15) + except Exception as exc: + raise RuntimeError( + f"Cerebral: embedding model {embeddings_model!r} is not available. " + f"Set MEMORY_EMBEDDINGS_MODEL to a valid litellm embedding model. " + f"Error: {exc}" + ) from exc + + # Start a dedicated event loop in a background thread. + # The MemoryEngine's asyncpg pool binds to this loop; it must + # outlive every operation, so we never close it until close(). + self._loop = asyncio.new_event_loop() + self._loop_thread = threading.Thread( + target=self._loop.run_forever, daemon=True, name="cerebral-loop" + ) + self._loop_thread.start() + + # Register cost recorder before initializing MemoryEngine so + # all LLM calls (including the probe) are captured. + cost_recorder = register_cerebral_cost_recorder() + + # Resolve embedding input limits (auto-detect for Bedrock Cohere v3) + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_input_chars, extra_kwargs = resolve_embedding_input_limits( + embeddings_model, embeddings_max_input_chars + ) + + # Use metered embeddings that capture usage costs + base_embeddings = LiteLLMSDKEmbeddings( + model=embeddings_model, + api_key=None, # litellm reads from env + ) + metered_embeddings = MeteredLiteLLMSDKEmbeddings( + base=base_embeddings, + cost_recorder=cost_recorder, + max_input_chars=max_input_chars, + extra_kwargs=extra_kwargs, + ) + + # Separate reflect LLM (Prefrontal reflect + mental-model refresh), only + # when configured; otherwise Hindsight falls back to memory_llm_*. + reflect_kwargs: dict[str, str | None] = {} + if reflect_llm_model: + reflect_kwargs = { + "reflect_llm_provider": llm_provider, + "reflect_llm_model": reflect_llm_model, + "reflect_llm_api_key": reflect_llm_api_key, + "reflect_llm_base_url": reflect_llm_base_url or None, + } + + # Separate consolidation LLM, only when configured; otherwise Hindsight + # falls back to memory_llm_*. + consolidation_kwargs: dict[str, str | None] = {} + if consolidation_llm_model: + consolidation_kwargs = { + "consolidation_llm_provider": llm_provider, + "consolidation_llm_model": consolidation_llm_model, + "consolidation_llm_api_key": consolidation_llm_api_key, + "consolidation_llm_base_url": consolidation_llm_base_url or None, + } + + # Run consolidation and mental-model refreshes inline. The default + # BrokerTaskBackend only queues rows for a WorkerPoller, which + # codespy never starts, so they would never run. + task_backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + self._engine = MemoryEngine( + db_url=database_url, + memory_llm_provider=llm_provider, + memory_llm_model=llm_model, + memory_llm_api_key=llm_api_key, + memory_llm_base_url=llm_base_url or None, + **reflect_kwargs, + **consolidation_kwargs, + embeddings=metered_embeddings, + cross_encoder=RRFPassthroughCrossEncoder(), + tenant_extension=DefaultTenantExtension(config={"schema": HINDSIGHT_SCHEMA}), + skip_llm_verification=True, + task_backend=task_backend, + ) + + # Attach engine to task backend so it can mark failed operations + task_backend.attach(self._engine) + + # Install two-step structured output fallback when extraction model is configured + if extraction_llm_model: + try: + from hindsight_api.engine.llm_wrapper import LLMConfig + from codespy.agents.memory.cerebral.structured_output import ( + install_two_step_structured_output, + ) + + extraction_cfg = LLMConfig( + provider="litellm", + model=extraction_llm_model, + api_key=extraction_llm_api_key, + base_url=extraction_llm_base_url or "", + reasoning_effort=None, + ) + extraction_impl = extraction_cfg._provider_impl + install_two_step_structured_output(self._engine, extraction_impl) + except Exception: + logger.warning( + "Failed to install two-step structured output for extraction model %s", + extraction_llm_model, + exc_info=True, + ) + + self._run_async(self._engine.initialize()) + + # One-time repair: install maintenance routines if missing. + # This is idempotent and only needed for databases that were migrated + # before HINDSIGHT_API_DATABASE_SCHEMA was set to "semantic" (upstream #2638). + self._run_async(ensure_maintenance_routines(self._engine, HINDSIGHT_SCHEMA)) + + # Repair orphaned pending operations left by inline SyncTaskBackend + from codespy.agents.memory.cerebral.orphans import repair_orphaned_operations + + self._run_async( + repair_orphaned_operations(self._engine, HINDSIGHT_SCHEMA, bank_id) + ) + + self._bank_id = bank_id + self._bank_ensured = False + self._bank_exists = False + self._missing_bank_logged = False + self._retain_chunk_size = retain_chunk_size + self._mental_models = mental_models + self._max_mental_model_tokens = max_mental_model_tokens + self._min_mental_model_refresh_seconds = min_mental_model_refresh_seconds + self._max_observations_per_scope = max_observations_per_scope + # Mental-model ids already ensured by this process. + self._ensured_mental_models: set[str] = set() + # Lock for consolidation serialization + self._consolidation_lock = threading.Lock() + # Pending scopes per run_id for one-consolidation-per-run (dict[str, list[list[str]]]) + self._pending_scopes: dict[str, list[list[str]]] = {} + # Lock for _pending_scopes (written from background save threads, popped on pipeline thread) + self._pending_scopes_lock = threading.Lock() + + logger.info( + "Cerebral MemoryEngine initialized (schema=%s, bank=%s, embeddings=%s, provider=%s, chunk_size=%s, max_input_chars=%s)", + HINDSIGHT_SCHEMA, bank_id, embeddings_model, llm_provider, retain_chunk_size, + max_input_chars, + ) + + def _run_async(self, coro): + """Submit a coroutine to the dedicated event loop and block until done.""" + future = asyncio.run_coroutine_threadsafe(coro, self._loop) + return future.result() + + async def _await(self, coro): + """Await a coroutine on the dedicated loop without blocking the caller's loop. + + When already running on the dedicated loop, the coroutine is awaited + directly (``run_coroutine_threadsafe`` + wait would deadlock there). + """ + try: + running = asyncio.get_running_loop() + except RuntimeError: + running = None + if running is self._loop: + return await coro + return await asyncio.wrap_future(asyncio.run_coroutine_threadsafe(coro, self._loop)) + + def close(self) -> None: + """Shut down the MemoryEngine and stop the event loop.""" + try: + self._run_async(self._engine.close()) + except Exception: + logger.debug("cerebral: error closing engine", exc_info=True) + self._loop.call_soon_threadsafe(self._loop.stop) + self._loop_thread.join(timeout=10) + + def _ensure_bank(self) -> None: + """Create the Hindsight bank and configure it (idempotent).""" + if self._bank_ensured: + return + ctx = RequestContext() + try: + self._run_async( + self._engine.ensure_bank_profile( + self._bank_id, + request_context=ctx, + ) + ) + self._bank_exists = True + except Exception: + logger.debug("cerebral: bank %s profile ensure failed", self._bank_id) + + try: + updates: dict[str, Any] = { + "retain_extraction_mode": "concise", + # See CerebralRetainConfig.chunk_size for constraint details. + "retain_chunk_size": self._retain_chunk_size, + "retain_mission": ( + "Retain durable knowledge learned while working on a task, whatever the domain. " + "Focus on how the subject is structured, the entities involved and how they relate, " + "domain rules, constraints and constants, procedures that proved effective, " + "data formats, and findings or results worth reusing later. " + "Each line starts with a [section] label naming the kind of knowledge, " + "or [artifact:name] for a produced output. " + "State each fact about the subject itself, keep names, identifiers, quantities " + "and references exact, and skip transient process notes that will not hold beyond this run.\n" + "Observation lines use these markers:\n" + "- A plain line is a newly learned fact.\n" + "- '(supersedes: X)': the line is the current wording of a fact previously worded X. " + "Update that fact; do not keep both.\n" + "- 'RETRACTED — shown incorrect or misleading': the fact was proven wrong or misled " + "the agent. Mark it invalid and do not rely on it.\n" + "A fact missing from a batch is unchanged, not outdated. Only RETRACTED invalidates a fact." + ), + "observations_mission": OBSERVATIONS_MISSION, + # Disable auto-consolidation; Cerebral triggers it explicitly + # after retain with proper scope scoping (prevents unscoped + # pending rows from blocking scoped submissions). + "enable_auto_consolidation": False, + } + # Per-scope observation cap (not bank-wide; -1 means unlimited) + if self._max_observations_per_scope >= 0: + updates["observation_scope_limits"] = [ + { + "scope": [f"{TAG_ORG}*", f"{TAG_REPO}*", f"{TAG_PROJECT_SCOPE}*"], + "limit": self._max_observations_per_scope, + } + ] + self._run_async( + self._engine.update_bank_config( + self._bank_id, + updates=updates, + request_context=ctx, + ) + ) + except Exception: + logger.warning("cerebral: failed to configure bank %s", self._bank_id, exc_info=True) + self._bank_ensured = True + + @staticmethod + def _mutation_lines(episode: Episode) -> list[str]: + """Build observation lines from episode mutations. + + Groups mutations by observation_id, keeping their order. `first` and + `last` are the first and last mutation in the group, and `final` is the + observation's content at the end of the run. + + Line format by mutation group (in order): + + | Group (in order) | Line | + |---|---| + | has ADD, in `context_memory` | `[s] ` | + | has ADD, last is EVICT | `[s] `, where final = `last.previous_content` | + | has ADD, last is DELETE | nothing | + | no ADD, last is REPLACE, in `context_memory` | `[s] (supersedes: )` | + | no ADD, has a REPLACE, last is EVICT | `[s] (supersedes: )` | + | no ADD, EVICT only | nothing | + | no ADD, last is DELETE | `[s] RETRACTED — shown incorrect or misleading, do not rely on: ` | + + Section `s`: for a group whose last mutation is EVICT or DELETE, use + `last.section`. Otherwise use the section `find_observation` returns. + + Returns list of formatted lines for the observations blob. + """ + from codespy.agents.memory.hippocampus.context_memory import MutationType + + if not episode.mutations: + return [] + + # Group mutations by observation_id, preserving order + groups: dict[str, list] = defaultdict(list) + for mut in episode.mutations: + groups[mut.observation_id].append(mut) + + lines: list[str] = [] + context_memory = episode.context_memory + + for obs_id, mutations in groups.items(): + first_mut = mutations[0] + last_mut = mutations[-1] + + # Check if this observation has an ADD + has_add = any(mut.type == MutationType.ADD for mut in mutations) + + if has_add: + # ADD in the mutation group + if last_mut.type == MutationType.DELETE: + # ADD → DELETE: emit nothing + continue + elif last_mut.type == MutationType.EVICT: + # ADD → EVICT: emit line with evicted content (previous_content) + lines.append(f"[{last_mut.section}] {last_mut.previous_content}") + else: + # ADD (possibly with REPLACEs), still in context_memory + found = context_memory.find_observation(obs_id) + if found: + section, obs = found + lines.append(f"[{section}] {obs.content}") + else: + # No ADD in the group: prior observation + if last_mut.type == MutationType.DELETE: + # DELETE only: RETRACTED line + lines.append( + f"[{last_mut.section}] RETRACTED — shown incorrect or misleading, " + f"do not rely on: {first_mut.previous_content}" + ) + elif any(mut.type == MutationType.REPLACE for mut in mutations): + # Has at least one REPLACE (last could be REPLACE or EVICT) + if last_mut.type == MutationType.EVICT: + # REPLACE → EVICT: emit line with evicted content + supersedes + # Find the EVICT to get its previous_content + evict_mut = next( + mut for mut in reversed(mutations) if mut.type == MutationType.EVICT + ) + lines.append( + f"[{last_mut.section}] {evict_mut.previous_content} " + f"(supersedes: {first_mut.previous_content})" + ) + else: + # REPLACE only, still in context_memory + found = context_memory.find_observation(obs_id) + if found: + section, obs = found + lines.append( + f"[{section}] {obs.content} (supersedes: {first_mut.previous_content})" + ) + elif last_mut.type == MutationType.EVICT: + # EVICT only: emit nothing (unchanged prior fact) + pass + + return lines + + @staticmethod + def _retain_metadata(episode: Episode, repo_full_name: str | None, kind: str) -> dict[str, str]: + """Build metadata dict for a retain item. + + Args: + episode: The episode being retained. + repo_full_name: Full repo name (owner/repo) or None. + kind: "observation changes" or "artifacts". + + Returns: + Dict with task, repo (when known), kind, and question (episode.question). + """ + meta: dict[str, str] = { + "task": episode.task, + "kind": kind, + "question": episode.question, + } + if repo_full_name: + meta["repo"] = repo_full_name + return meta + + def retain_episode(self, episode: Episode, repo_full_name: str | None = None) -> None: + """Retain episode mutations and artifacts in Hindsight semantic memory. + + The observations blob is built from episode.mutations instead of + context_memory, capturing ADD/REPLACE/DELETE operations as: + - Plain lines for ADDs still in context_memory + - "(supersedes:)" lines for REPLACEs + - "RETRACTED" lines for DELETEs + + With ``repo_full_name``, items also get ``repo:``/``org:`` tags and an + explicit ``observation_scopes`` list (one scope per ``project_scope`` + topic, never ``task:``/``episode:``/``run_id:``/``pull_request:``), so + observations merge across tasks and runs. Consolidation runs inline + (``SyncTaskBackend``), then the mental models of the touched scopes + are ensured. Without a repo, today's behaviour is kept. + """ + self._ensure_bank() + + tags = self._build_tags(episode, repo_full_name) + scopes = self._observation_scopes(episode, repo_full_name) + episode_doc_id = f"episode-{episode.id}" + + # Build contents list: at most TWO items per episode + contents: list[dict] = [] + + # Build observations blob from mutations + obs_lines = self._mutation_lines(episode) + if obs_lines: + contents.append({ + "content": "\n\n".join(obs_lines), + "metadata": self._retain_metadata(episode, repo_full_name, "observation changes"), + "tags": tags, + "document_id": episode_doc_id, + "event_date": episode.timestamp.isoformat(), + }) + + # Merge all artifacts into a single content item (unchanged) + artifact_lines: list[str] = [] + for name, content in (episode.artifacts or {}).items(): + artifact_lines.append(f"[artifact:{name}] {content}") + if artifact_lines: + contents.append({ + "content": "\n\n".join(artifact_lines), + "metadata": self._retain_metadata(episode, repo_full_name, "artifacts"), + "tags": tags, + "document_id": episode_doc_id, + "event_date": episode.timestamp.isoformat(), + }) + + if not contents: + logger.debug("cerebral: no items for episode %s", episode.id) + return + + if scopes is not None: + for item in contents: + item["observation_scopes"] = [list(sc) for sc in scopes] + + # Sync briefing triggers BEFORE retain+consolidation so consolidation + # uses the correct refresh settings from the start. + mental_model_scopes = self._mental_model_scopes(scopes or [], repo_full_name) if repo_full_name else [] + self._sync_briefing_triggers(mental_model_scopes) + + started = time.monotonic() + try: + self._run_async( + self._engine.retain_batch_async( + bank_id=self._bank_id, + contents=contents, + request_context=RequestContext(), + ) + ) + except Exception: + logger.warning( + "cerebral: failed to retain episode %s", + episode.id, exc_info=True, + ) + return + + # Record scopes for one-consolidation-per-run instead of submitting inline + # Skip when repo_full_name is None (matches today's behavior: no scopes means no consolidation) + if repo_full_name and mental_model_scopes: + with self._pending_scopes_lock: + # Get existing scopes for this run_id (deduped by sorted tuple) + existing = self._pending_scopes.get(episode.run_id, []) + seen: set[tuple[str, ...]] = {tuple(sorted(s)) for s in existing} + for scope_tags in mental_model_scopes: + key = tuple(sorted(scope_tags)) + if key not in seen: + seen.add(key) + existing.append(scope_tags) + self._pending_scopes[episode.run_id] = existing + + logger.info( + "cerebral: retained %d content blobs for episode %s (bank=%s, task=%s) in %.1fs; " + "consolidation deferred to end of run %s", + len(contents), episode.id, self._bank_id, episode.task, + time.monotonic() - started, episode.run_id, + ) + + @staticmethod + def _observation_scopes(episode: Episode, repo_full_name: str | None) -> list[list[str]] | None: + """Observation scopes for consolidation, or None without a repo (Hindsight default). + + One ``[org:, repo:, project_scope:]`` scope per ``project_scope`` topic, + or ``[org:, repo:, project_scope:]`` (repo-root scope) when the + episode has none. The repo-root scope is what ``make_topic_id(repo, '.')`` + returns, so it matches the scope a root-level PR would use. + """ + if not repo_full_name: + return None + scopes = [ + scope_tags(repo_full_name, topic.id) + for topic in episode.context_memory.topics + if topic.type == "project_scope" + ] + # Fallback to repo-root project scope instead of bare [org:, repo:] + return scopes or [scope_tags(repo_full_name, repo_full_name)] + + @staticmethod + def _mental_model_scopes( + scopes: Sequence[Sequence[str]], repo_full_name: str + ) -> list[list[str]]: + """Observation scopes plus the repo scope ``[org:, repo:]`` (always added, deduped).""" + out: list[list[str]] = [] + seen: set[tuple[str, ...]] = set() + for sc in [*scopes, repo_tags(repo_full_name)]: + key = tuple(sorted(sc)) + if key not in seen: + seen.add(key) + out.append(list(sc)) + return out + + @staticmethod + def _mental_model_name(tags: Sequence[str]) -> str: + for tag in tags: + if tag.startswith(TAG_PROJECT_SCOPE): + return f"Briefing: {tag[len(TAG_PROJECT_SCOPE):]}" + for tag in tags: + if tag.startswith("repo:"): + return f"Briefing: {tag[len('repo:'):]}" + return "Briefing" + + def _sync_briefing_triggers(self, scopes: list[list[str]]) -> None: + """Sync briefing triggers to match current settings before consolidation. + + Runs before retain+consolidation so that consolidation uses the correct + refresh settings. The desired trigger combines: + - Static caps from _BRIEFING_TRIGGER_STATIC (delta mode, chunk/entity caps) + - Dynamic settings: refresh_after_consolidation, min_refresh_interval_seconds + + For existing briefings, updates the trigger if it differs. For missing + briefings, creates them only when mental_models is enabled. + """ + if not scopes: + return + + desired_trigger = { + **_BRIEFING_TRIGGER_STATIC, + "refresh_after_consolidation": self._mental_models, + "min_refresh_interval_seconds": self._min_mental_model_refresh_seconds, + } + + for tags in scopes: + mm_id = mental_model_id(tags) + if mm_id in self._ensured_mental_models: + continue + + try: + ctx = RequestContext() + existing = self._run_async( + self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx) + ) + + if existing is None: + # Create missing briefing only when mental_models is enabled + if not self._mental_models: + self._ensured_mental_models.add(mm_id) + continue + + try: + self._run_async( + self._engine.create_mental_model( + self._bank_id, + self._mental_model_name(tags), + MENTAL_MODEL_SOURCE_QUERY, + MENTAL_MODEL_PLACEHOLDER, + mental_model_id=mm_id, + tags=sorted(tags), + max_tokens=self._max_mental_model_tokens, + trigger=desired_trigger, + request_context=ctx, + ) + ) + logger.info( + "cerebral: created briefing %s (%s) refresh=%s", + mm_id, + self._mental_model_name(tags), + "on" if self._mental_models else "off", + ) + except OperationValidationError as exc: + if getattr(exc, "status_code", None) != 409: + raise + # 409 means another process created it; fall through to update + existing = self._run_async( + self._engine.get_mental_model(self._bank_id, mm_id, request_context=ctx) + ) + else: + # Existing briefing: update trigger if it differs + current_trigger = existing.get("trigger") or {} + needs_update = ( + current_trigger.get("refresh_after_consolidation") != self._mental_models + or current_trigger.get("exclude_mental_models") is not True + ) + if needs_update: + self._run_async( + self._engine.update_mental_model( + self._bank_id, + mm_id, + trigger=desired_trigger, + request_context=ctx, + ) + ) + logger.info( + "cerebral: briefing %s refresh %s -> %s", + mm_id, + "on" if current_trigger.get("refresh_after_consolidation") else "off", + "on" if self._mental_models else "off", + ) + + self._ensured_mental_models.add(mm_id) + except Exception: + logger.warning("cerebral: failed to sync briefing trigger for %s", mm_id, exc_info=True) + + def consolidate_run(self, run_id: str) -> None: + """Trigger one consolidation for all scopes touched during a run. + + Called by the pipeline after join_episode_saves() completes. + Submits a single consolidation with the union of all scopes, + which Hindsight ORs and refreshes affected mental models once. + Never raises. + + Args: + run_id: The run identifier to consolidate scopes for. + """ + # Pop first so the run's entry is always released, even on failure. + with self._pending_scopes_lock: + scopes = self._pending_scopes.pop(run_id, None) + + if not scopes: + logger.info("consolidate_run: no scopes pending for run %s; skipped", run_id) + return + + try: + from codespy.agents.cost_tracker import get_cost_tracker + + tracker = get_cost_tracker() + + def _calls(bucket: str) -> int: + stats = tracker.get_signature_stats(bucket) + return stats.call_count if stats else 0 + + buckets = (BUCKET_MEMORY_CONSOLIDATION, BUCKET_MEMORY_MENTAL_MODELS, BUCKET_MEMORY_OTHER) + baseline = {b: _calls(b) for b in buckets} + + logger.info( + "consolidate_run: starting for run %s with %d scope(s)", + run_id, len(scopes), + ) + started = time.monotonic() + + with self._consolidation_lock: + # Submit one consolidation with all scopes (Hindsight ORs them) + self._run_async( + self._engine.submit_async_consolidation( + self._bank_id, + request_context=RequestContext(), + observation_scopes=scopes, + ) + ) + + delta = {b: _calls(b) - baseline[b] for b in buckets} + logger.info( + "consolidate_run: finished for run %s in %.1fs, " + "memory_consolidation +%d calls, memory_mental_models +%d calls, memory_other +%d calls", + run_id, + time.monotonic() - started, + delta[BUCKET_MEMORY_CONSOLIDATION], + delta[BUCKET_MEMORY_MENTAL_MODELS], + delta[BUCKET_MEMORY_OTHER], + ) + except Exception: + logger.warning( + "consolidate_run: consolidation failed for run %s", + run_id, + exc_info=True, + ) + + # ------------------------------------------------------------------ + # Read API (Prefrontal) + # ------------------------------------------------------------------ + + async def _abank_exists(self) -> bool: + """Check whether the bank exists in Hindsight. + + Returns True immediately if already known to exist. Otherwise queries + the engine and caches the result. "Missing" is not cached: a later + retain creates the bank, and subsequent reads must see it. + """ + if self._bank_exists: + return True + profile = await self._await( + self._engine.get_bank_profile(self._bank_id, request_context=RequestContext()) + ) + if profile is not None: + self._bank_exists = True + return True + if not self._missing_bank_logged: + self._missing_bank_logged = True + logger.info( + "cerebral: bank %s not created yet (nothing retained); reads return empty", + self._bank_id, + ) + return False + + async def arecall( + self, + query: str, + *, + tag_groups: list[Any] | None = None, + max_tokens: int = 4096, + fact_type: list[str] | None = None, + prefer_observations: bool = False, + budget: str | Budget = Budget.MID, + question_date: datetime | None = None, + ) -> list[Any]: + """Raw recall (one embedding call, no LLM). Returns ``MemoryFact`` list.""" + if not await self._abank_exists(): + return [] + result = await self._await( + self._engine.recall_async( + self._bank_id, + query, + budget=Budget(budget), + max_tokens=max_tokens, + fact_type=list(fact_type) if fact_type else None, + prefer_observations=prefer_observations, + question_date=question_date, + tag_groups=tag_groups, + request_context=RequestContext(), + ) + ) + return list(getattr(result, "results", None) or []) + + def recall(self, query: str, **kwargs: Any) -> list[Any]: + """Blocking variant of :meth:`arecall`.""" + return self._run_async(self.arecall(query, **kwargs)) + + async def areflect( + self, + query: str, + *, + tag_groups: list[Any] | None = None, + max_tokens: int = 4096, + context: str | None = None, + fact_types: list[str] | None = None, + ) -> tuple[str, ReflectSummary]: + """Hindsight reflect loop (LLM). ``budget=LOW`` so the iteration cap is exactly + ``HINDSIGHT_API_REFLECT_MAX_ITERATIONS // 2`` (the LOW budget multiplier is 0.5). + + Args: + query: The query to reflect on. + tag_groups: Optional tag groups to filter by. + max_tokens: Maximum tokens for the response. + context: Optional context string for the reflection. + fact_types: Optional list of fact types to consider. + + Returns: + Tuple of (text, ReflectSummary). The summary records iterations, LLM calls, + map-call count (for split synthesis detection), rewrite, tools used, usage, + and whether the text is empty. + """ + if not await self._abank_exists(): + return "", ReflectSummary() + reflect_kwargs: dict[str, Any] = { + "budget": Budget.LOW, + "context": context, + "max_tokens": max_tokens, + "tag_groups": tag_groups, + "fact_types": list(fact_types) if fact_types else None, + "request_context": RequestContext(), + } + + result = await self._await( + self._engine.reflect_async(self._bank_id, query, **reflect_kwargs) + ) + text = (getattr(result, "text", None) or "").strip() + summary = self._build_reflect_summary(result) + return text, summary + + def _build_reflect_summary(self, result: Any) -> ReflectSummary: + """Build ReflectSummary from Hindsight ReflectResult. + + The ReflectResult shape from hindsight_api: + - llm_trace: list[LLMCallTrace(scope, duration_ms)] + - tool_trace: list[ToolCallTrace(tool, input, output: dict, iteration, ...)] + - usage: TokenUsage(input_tokens, output_tokens, thoughts_tokens) + """ + import json + + summary = ReflectSummary() + if result is None: + return summary + + # LLM trace is a list of LLMCallTrace objects with 'scope' attribute + llm_trace = getattr(result, "llm_trace", None) or [] + scope_names: list[str] = [] + for call in llm_trace: + scope = getattr(call, "scope", None) + if scope: + scope_names.append(str(scope)) + summary.llm_calls += 1 + if str(scope).startswith("final_map_"): + summary.map_calls += 1 + elif str(scope) == "final_rewrite": + summary.rewrite = True + + # Count iterations from agent_ scopes (excluding _err variants) + # +1 if there's any final scope (map, reduce, or rewrite) + agent_scopes = {s for s in scope_names if s.startswith("agent_") and not s.endswith("_err")} + summary.iterations = len(agent_scopes) + if any(s.startswith("final") for s in scope_names): + summary.iterations += 1 + + # Tools from tool_trace (list of ToolCallTrace with output dict) + tool_trace = getattr(result, "tool_trace", None) or [] + for tc in tool_trace: + tool_name = getattr(tc, "tool", "") or "" + output = getattr(tc, "output", None) or {} + # Estimate tokens from serialized output (same heuristic as prefrontal.format) + try: + output_text = json.dumps(output) if output else "" + except Exception: + output_text = str(output) + output_tokens = (len(output_text) + 3) // 4 # chars/4 heuristic + summary.tools.append((tool_name, output_tokens)) + + # Usage from TokenUsage + usage = getattr(result, "usage", None) + if usage: + summary.usage = { + "input_tokens": getattr(usage, "input_tokens", 0) or 0, + "output_tokens": getattr(usage, "output_tokens", 0) or 0, + "thoughts_tokens": getattr(usage, "thoughts_tokens", 0) or 0, + } + + summary.empty = not (getattr(result, "text", None) or "").strip() + return summary + + def reflect(self, query: str, **kwargs: Any) -> tuple[str, ReflectSummary]: + """Blocking variant of :meth:`areflect`.""" + return self._run_async(self.areflect(query, **kwargs)) + + async def aget_mental_models(self, ids: Sequence[str]) -> list[dict]: + """Fetch mental models by id (no LLM). Missing ids are skipped; order is kept.""" + if not await self._abank_exists(): + return [] + async def _get_all() -> list[Any]: + return await asyncio.gather( + *( + self._engine.get_mental_model( + self._bank_id, mm_id, request_context=RequestContext() + ) + for mm_id in ids + ), + return_exceptions=True, + ) + + results = await self._await(_get_all()) + models: list[dict] = [] + for mm_id, res in zip(ids, results, strict=True): + if isinstance(res, BaseException): + logger.debug("cerebral: get_mental_model %s failed: %s", mm_id, res) + continue + if res: + models.append(res) + return models + + async def aget_observation_history(self, memory_id: str) -> list[dict]: + """History of an observation, most recent first (no LLM, no embedding).""" + if not await self._abank_exists(): + return [] + result = await self._await( + self._engine.get_observation_history( + self._bank_id, memory_id, request_context=RequestContext() + ) + ) + return list(result or []) + + @staticmethod + def _build_tags(episode: Episode, repo_full_name: str | None = None) -> list[str]: + """Build tags for retain items. + + Includes project_scope topics and repo/org tags. Excludes episode:, + run_id:, and pull_request: tags — they are not used for scoping or + reach and bloat the tag set. + """ + tags: list[str] = [] + for topic in episode.context_memory.topics: + if topic.type == "project_scope": + tags.append(f"{topic.type}:{topic.id}") + if repo_full_name: + tags.extend(repo_tags(repo_full_name)) + tags.append(f"task:{episode.task}") + return tags diff --git a/src/codespy/agents/memory/cerebral/cost.py b/src/codespy/agents/memory/cerebral/cost.py new file mode 100644 index 0000000..0b5f714 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/cost.py @@ -0,0 +1,509 @@ +"""Cost tracking for Cerebral (Hindsight) LLM calls and embeddings. + +Uses the Hindsight span recorder to intercept LLM calls made by MemoryEngine +and price them using litellm.cost_per_token. Embeddings are metered by +wrapping the LiteLLM SDK with a proxy that captures usage. +""" + +from __future__ import annotations + +import logging +import re +import threading +import time +from contextvars import ContextVar +from typing import TYPE_CHECKING, Any + +from codespy.agents.memory.recall import current_recall_usage +from codespy.config_memory import ( + MEMORY_CONSOLIDATION, + MEMORY_EMBEDDINGS, + MEMORY_MENTAL_MODELS, + MEMORY_OTHER, + MEMORY_RECALL, + MEMORY_RETAIN, +) + +if TYPE_CHECKING: + from hindsight_api.engine.embeddings import LiteLLMSDKEmbeddings + +logger = logging.getLogger(__name__) + +# Track if we've warned about truncating inputs (once per process) +_warned_truncation: bool = False + +# Module-level lock for thread-safe registration +_recorder_lock = threading.Lock() +_recorder_registered = False + +# Task type constants for contextvar tagging +TASK_CONSOLIDATION = "consolidation" +TASK_REFRESH_MENTAL_MODEL = "refresh_mental_model" + +# ContextVar to tag the current memory task (consolidation, refresh_mental_model, etc.) +# This is set by _SchemaSyncTaskBackend.submit_task around inline task execution. +current_memory_task: ContextVar[str | None] = ContextVar("current_memory_task", default=None) + +# Buckets for cost attribution (using memory_* naming to match config/env names) +BUCKET_MEMORY_RETAIN = MEMORY_RETAIN +BUCKET_MEMORY_OTHER = MEMORY_OTHER +BUCKET_MEMORY_EMBEDDINGS = MEMORY_EMBEDDINGS +# Recall reads (recall/reflect inside a track_recall_usage() block) +BUCKET_MEMORY_RECALL = MEMORY_RECALL +# Recall embeddings (separate bucket for embeddings during recall) +BUCKET_MEMORY_RECALL_EMBEDDINGS = f"{MEMORY_RECALL}_embeddings" +# Consolidation task LLM calls +BUCKET_MEMORY_CONSOLIDATION = MEMORY_CONSOLIDATION +# Mental model refresh task LLM calls +BUCKET_MEMORY_MENTAL_MODELS = MEMORY_MENTAL_MODELS + +# Track models we've warned about missing prices (one warning per model) +_warned_unpriced_models: set[str] = set() + + +def _llm_bucket(scope: str) -> str: + """Determine the cost bucket for an LLM call based on scope and context. + + Precedence (first match wins): + 1. current_recall_usage set → memory_recall (handled in record_llm_call) + 2. current_memory_task == "refresh_mental_model" → memory_mental_models + 3. current_memory_task == "consolidation" → memory_consolidation + 4. scope starts with "retain" → memory_retain + 5. Scope fallback when no task tag is set: + - consolidation* → memory_consolidation + - mental_model_* → memory_mental_models + 6. Otherwise → memory_other + + Args: + scope: The Hindsight scope string (e.g., "retain_extract_facts", "consolidation") + + Returns: + The cost bucket name (one of the BUCKET_MEMORY_* constants) + """ + # Check current_memory_task first (set by _SchemaSyncTaskBackend for inline tasks) + task = current_memory_task.get() + if task == TASK_REFRESH_MENTAL_MODEL: + return BUCKET_MEMORY_MENTAL_MODELS + if task == TASK_CONSOLIDATION: + return BUCKET_MEMORY_CONSOLIDATION + + # Scope-based routing + if scope and scope.startswith("retain"): + return BUCKET_MEMORY_RETAIN + + # Scope fallback for known patterns when no task tag is set + # (e.g., if Hindsight runs refresh via create_task without context copy) + if scope: + if scope.startswith("consolidation"): + return BUCKET_MEMORY_CONSOLIDATION + if scope.startswith("mental_model_"): + return BUCKET_MEMORY_MENTAL_MODELS + + return BUCKET_MEMORY_OTHER + + +class CerebralCostRecorder: + """Records Hindsight LLM calls into the CostTracker. + + Registered as a span recorder with Hindsight's CompositeSpanRecorder. + Receives every LLM call made by MemoryEngine (retain, consolidation, etc.) + and accumulates costs via CostTracker.add_external_call. + """ + + def record_llm_call( # type: ignore[return] + self, + *, + model: str = "", + scope: str = "", + input_tokens: int = 0, + output_tokens: int = 0, + error: Exception | None = None, + duration: float = 0.0, + **_: Any, + ) -> None: + """Record a completed LLM call. + + Args: + model: Full litellm model string (e.g., "bedrock/converse/moonshotai.kimi-k2.5") + scope: Hindsight scope (e.g., "retain_extract_facts", "consolidation") + input_tokens: Input/prompt token count + output_tokens: Output/completion token count + error: Exception if the call failed + duration: Duration of the call in seconds + **_: Ignored extra kwargs for forward compatibility + + Note: + Provider-internal retries are not counted — only successful calls + and errored calls that reached the provider (with billed usage). + """ + try: + # Skip calls with no tokens (includes failed calls that never reached provider) + if input_tokens == 0 and output_tokens == 0: + return + + # Price the call using litellm + prompt_cost, completion_cost = self._price_call_split(model, input_tokens, output_tokens) + cost = prompt_cost + completion_cost + tokens = input_tokens + output_tokens + + # Determine bucket: an active recall wins over the scope/task tag, + # because Hindsight scopes cannot tell a reflect apart + # from a mental-model refresh. + usage = current_recall_usage.get() + if usage is not None: + usage.add_llm(model, input_tokens, output_tokens, prompt_cost, completion_cost) + bucket = BUCKET_MEMORY_RECALL + else: + bucket = _llm_bucket(scope) + + # Import here to avoid circular imports at module load + from codespy.agents.cost_tracker import get_cost_tracker + + tracker = get_cost_tracker() + tracker.add_external_call( + bucket, + cost=cost, + tokens=tokens, + calls=1, + input_tokens=input_tokens, + output_tokens=output_tokens, + input_cost=prompt_cost, + output_cost=completion_cost, + duration=duration, + ) + + except Exception as e: + # Metering must never break the actual operation + logger.debug("CerebralCostRecorder failed to record call: %s", e) + + def _price_call(self, model: str, input_tokens: int, output_tokens: int) -> float: + """Price a call using litellm.cost_per_token. + + Returns 0.0 if the model has no price registered. Logs a warning + once per unpriced model. + + Deprecated: Use _price_call_split for input/output cost breakdown. + """ + prompt_cost, completion_cost = self._price_call_split(model, input_tokens, output_tokens) + return prompt_cost + completion_cost + + def _price_call_split( + self, model: str, input_tokens: int, output_tokens: int + ) -> tuple[float, float]: + """Price a call using litellm.cost_per_token. + + Returns (prompt_cost, completion_cost). Returns (0.0, 0.0) if the model + has no price registered. Logs a warning once per unpriced model. + + Returns: + Tuple of (input/prompt cost, output/completion cost) + """ + try: + import litellm + + prompt_cost, completion_cost = litellm.cost_per_token( + model=model, + prompt_tokens=input_tokens, + completion_tokens=output_tokens, + ) + return float(prompt_cost), float(completion_cost) + except Exception: + # Model may not have a price + if model not in _warned_unpriced_models: + _warned_unpriced_models.add(model) + logger.warning( + "No litellm price for model %s; cost will be $0. " + "Tokens are still recorded.", + model, + ) + return 0.0, 0.0 + + +class _LiteLLMProxy: + """Proxy for the litellm module that meters embedding calls. + + Intercepts ``aembedding`` to capture usage and record costs before + returning the response. All other attributes forward to the real litellm. + """ + + def __init__( + self, + real_litellm: Any, + cost_recorder: "CerebralCostRecorder", + max_input_chars: int | None = None, + extra_kwargs: dict[str, Any] | None = None, + ) -> None: + self._real = real_litellm + self._recorder = cost_recorder + self._max_input_chars = max_input_chars + self._extra_kwargs = extra_kwargs or {} + + async def aembedding(self, **kwargs: Any) -> Any: + """Call litellm.aembedding and meter the usage.""" + # Apply character cap to input if configured + if self._max_input_chars is not None: + kwargs = self._apply_input_cap(kwargs) + + # Merge extra_kwargs (explicit caller values win) + for key, value in self._extra_kwargs.items(): + kwargs.setdefault(key, value) + + start = time.perf_counter() + response = await self._real.aembedding(**kwargs) + elapsed = time.perf_counter() - start + self._meter_embedding(response, kwargs.get("model", ""), elapsed) + return response + + def _apply_input_cap(self, kwargs: dict[str, Any]) -> dict[str, Any]: + """Apply character cap to input strings. + + Creates a new input list with each string longer than the cap cut to + text[:cap]. Non-str items are left as-is. Logs a warning on first + truncation, then debug. + """ + global _warned_truncation + + input_data = kwargs.get("input") + if not isinstance(input_data, list): + return kwargs + + new_input: list[Any] = [] + max_seen = 0 + truncated_count = 0 + + for item in input_data: + if isinstance(item, str) and len(item) > self._max_input_chars: + new_input.append(item[: self._max_input_chars]) + max_seen = max(max_seen, len(item)) + truncated_count += 1 + else: + new_input.append(item) + if isinstance(item, str): + max_seen = max(max_seen, len(item)) + + if truncated_count > 0: + level = logging.WARNING if not _warned_truncation else logging.DEBUG + _warned_truncation = True + logger.log( + level, + "Truncated %d embedding input(s) to %d chars (largest was %d)", + truncated_count, + self._max_input_chars, + max_seen, + ) + + # Return new kwargs with capped input (don't mutate caller's dict) + return {**kwargs, "input": new_input} + + def __getattr__(self, name: str) -> Any: + """Forward all other attributes to the real litellm.""" + return getattr(self._real, name) + + def _meter_embedding(self, response: Any, model: str, duration: float = 0.0) -> None: + """Extract usage from embedding response and record cost.""" + try: + # Try _hidden_params["response_cost"] first (litellm populates this) + cost: float | None = None + if hasattr(response, "_hidden_params") and isinstance(response._hidden_params, dict): + cost = response._hidden_params.get("response_cost") + + # Fallback to litellm.completion_cost if available + if cost is None: + try: + import litellm + + cost = litellm.completion_cost(response) + except Exception: + cost = 0.0 + + # Token extraction: usage.prompt_tokens or usage.total_tokens + tokens = 0 + if hasattr(response, "usage") and response.usage: + usage = response.usage + if hasattr(usage, "prompt_tokens"): + tokens = usage.prompt_tokens + elif hasattr(usage, "total_tokens"): + tokens = usage.total_tokens + elif isinstance(usage, dict): + tokens = usage.get("prompt_tokens", 0) or usage.get("total_tokens", 0) + + if tokens == 0: + # Can't meter without tokens + return + + # Import here to avoid circular imports + from codespy.agents.cost_tracker import get_cost_tracker + + usage = current_recall_usage.get() + if usage is not None: + usage.add_embedding(tokens, cost or 0.0) + bucket = BUCKET_MEMORY_RECALL_EMBEDDINGS + else: + bucket = BUCKET_MEMORY_EMBEDDINGS + + tracker = get_cost_tracker() + # Embeddings have no output tokens - all tokens are input + tracker.add_external_call( + bucket, + cost=cost or 0.0, + tokens=tokens, + calls=1, + input_tokens=tokens, + output_tokens=0, + input_cost=cost or 0.0, + output_cost=0.0, + duration=duration, + ) + + except Exception as e: + logger.debug("Failed to meter embedding call: %s", e) + + +class MeteredLiteLLMSDKEmbeddings: + """Wrapper for LiteLLMSDKEmbeddings that meters costs. + + Proxies the underlying ``_litellm`` module after initialization +to capture embedding usage. The dimension detection probe during + ``initialize()`` is intentionally unmetered. + """ + + def __init__( + self, + *, + base: LiteLLMSDKEmbeddings, + cost_recorder: CerebralCostRecorder, + max_input_chars: int | None = None, + extra_kwargs: dict[str, Any] | None = None, + ) -> None: + self._base = base + self._recorder = cost_recorder + self._proxy_set = False + self._max_input_chars = max_input_chars + self._extra_kwargs = extra_kwargs or {} + + async def initialize(self) -> None: + """Initialize the base embeddings, then wrap _litellm.""" + # Run base initialization (this does the dimension probe) + await self._base.initialize() + + # Wrap _litellm after initialization to meter actual retain/recall + # calls, not the probe. Must check if already wrapped because + # initialize() may be called multiple times. + if not self._proxy_set and hasattr(self._base, "_litellm") and self._base._litellm is not None: + self._base._litellm = _LiteLLMProxy( + self._base._litellm, + self._recorder, + max_input_chars=self._max_input_chars, + extra_kwargs=self._extra_kwargs, + ) + self._proxy_set = True + + def __getattr__(self, name: str) -> Any: + """Forward all other attributes to the base embeddings.""" + return getattr(self._base, name) + + +def register_cerebral_cost_recorder() -> CerebralCostRecorder: + """Register the Cerebral cost recorder with Hindsight's span recorder. + + Idempotent: returns existing recorder if already registered. + Call unregister_cerebral_cost_recorder() to remove. + + Returns: + The registered CerebralCostRecorder instance. + """ + global _recorder_registered + + with _recorder_lock: + if _recorder_registered: + # Find and return existing recorder + try: + from hindsight_api.tracing import get_span_recorder + + recorder = get_span_recorder() + for r in getattr(recorder, "_recorders", []): + if isinstance(r, CerebralCostRecorder): + return r + except Exception: + pass + # If we can't find it, create a new one anyway + + recorder = CerebralCostRecorder() + try: + from hindsight_api.tracing import register_span_recorder + + register_span_recorder(recorder) + _recorder_registered = True + logger.debug("CerebralCostRecorder registered") + except Exception as e: + logger.warning("Failed to register CerebralCostRecorder: %s", e) + + return recorder + + +def unregister_cerebral_cost_recorder(recorder: CerebralCostRecorder | None = None) -> None: + """Unregister the Cerebral cost recorder. + + Args: + recorder: The recorder to unregister. If None, attempts to find + and unregister any CerebralCostRecorder in the composite. + """ + global _recorder_registered + + with _recorder_lock: + try: + from hindsight_api.tracing import get_span_recorder, unregister_span_recorder + + if recorder is not None: + unregister_span_recorder(recorder) + else: + # Find and unregister any CerebralCostRecorder + span_recorder = get_span_recorder() + for r in getattr(span_recorder, "_recorders", []): + if isinstance(r, CerebralCostRecorder): + unregister_span_recorder(r) + break + _recorder_registered = False + logger.debug("CerebralCostRecorder unregistered") + except Exception as e: + logger.debug("Failed to unregister CerebralCostRecorder: %s", e) + + +def resolve_embedding_input_limits( + model: str, override: int | None = None +) -> tuple[int | None, dict[str, Any]]: + """Resolve the maximum input characters and extra kwargs for embeddings. + + Bedrock Cohere embed v3 has a 2048 character limit that Hindsight does not + handle. This function returns the cap and any extra kwargs (e.g., truncate) + needed to stay within the limit. + + Args: + model: The litellm model string (e.g., "bedrock/cohere.embed-multilingual-v3"). + override: User override for the character cap: + - None (default): Auto-detect based on model (2048 for Bedrock Cohere v3). + - 0: Disable character capping (use Hindsight's token-based limit). + - N > 0: Use this specific character cap. + + Returns: + Tuple of (max_input_chars, extra_kwargs): + - max_input_chars: The character cap (None means no cap), or the override value. + - extra_kwargs: Dict of extra kwargs to pass to the embedding call + (e.g., {"truncate": "END"} for Cohere models). + """ + # Handle override + if override == 0: + return None, {} + if override is not None and override > 0: + # For Bedrock Cohere models, still add truncate even with custom cap + if re.search(r"bedrock/.*cohere\.embed-(english|multilingual)-v3", model): + return override, {"truncate": "END"} + return override, {} + + # Auto-detect based on model + # Match Bedrock Cohere v3 models (including region-prefixed like eu.cohere...) + if re.search(r"bedrock/.*cohere\.embed-(english|multilingual)-v3", model): + return 2048, {"truncate": "END"} + + # Default: no character cap (Hindsight's token-based limit applies) + return None, {} diff --git a/src/codespy/agents/memory/cerebral/delta_ops_compat.py b/src/codespy/agents/memory/cerebral/delta_ops_compat.py new file mode 100644 index 0000000..2250e12 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/delta_ops_compat.py @@ -0,0 +1,197 @@ +"""Compatibility shim for Hindsight delta operations parsing. + +The root cause of the "operations must be a list, got " error: + +When using models without native structured output support on Bedrock (e.g. +Nvidia Nemotron), litellm sends the schema as a forced synthetic +`json_tool_call` tool. The model returns the delta `operations` field as a +JSON-encoded string instead of a native array. + +For example, instead of: + {"operations": [{"op": "append_block", "section_id": "s1", "text": "..."}]} + +The model returns: + {"operations": '[{"op": "append_block", "section_id": "s1", "text": "..."}]'} + +Hindsight's `parse_delta_operation_list` expects `operations` to be a list +and raises `TypeError` when it's a string. This shim intercepts the raw +payload before Hindsight validates it, decoding any JSON-encoded operations. + +This shim can be removed once: +- Hindsight accepts string-encoded `operations` natively, OR +- The briefing model supports native structured output on Bedrock. + +See: hindsight_api/engine/reflect/delta_ops.py::parse_delta_operation_list +""" + +from __future__ import annotations + +import json +import logging +from typing import Any + +logger = logging.getLogger(__name__) + + +def _normalize(raw: Any) -> tuple[Any, bool]: + """Normalize delta operations payload, decoding JSON-encoded strings. + + Args: + raw: The raw payload, which may be a dict, string, or other type. + + Returns: + Tuple of (normalized_payload, changed). When `changed` is False, + the original `raw` is returned unchanged (identity preserved). + """ + # If raw is a string, try to parse it first + if isinstance(raw, str): + try: + from hindsight_api.engine.llm_wrapper import parse_llm_json + + parsed = parse_llm_json(raw) + return _normalize_dict_operations(parsed) + except (json.JSONDecodeError, ValueError): + # Not valid JSON - return unchanged, let original handler deal with it + return raw, False + + # If raw is a dict, normalize its operations field + if isinstance(raw, dict): + return _normalize_dict_operations(raw) + + # For other types (e.g., list), return unchanged + return raw, False + + +def _normalize_dict_operations(raw: dict[str, Any]) -> tuple[Any, bool]: + """Normalize a dict payload, handling string-encoded operations. + + Handles these cases: + 1. {"operations": "[...]"} -> {"operations": [...]} + 2. {"operations": '{"operations": [...]}'} -> {"operations": [...]} + 3. {"operations": ["{...}", "{...}"]} -> {"operations": [{...}, {...}]} + 4. Well-formed payloads pass through unchanged + """ + from hindsight_api.engine.llm_wrapper import parse_llm_json + + if "operations" not in raw: + return raw, False + + ops = raw["operations"] + + # Case 1 & 2: operations is a string that might be JSON-encoded + if isinstance(ops, str): + try: + decoded = parse_llm_json(ops) + except (json.JSONDecodeError, ValueError): + # Not valid JSON - log warning and return unchanged + logger.warning( + "cerebral: delta operations arrived as a non-JSON string; " + "first 200 chars: %s", + ops[:200], + ) + return raw, False + + # If decoded is a list, use it directly + if isinstance(decoded, list): + new_raw = dict(raw) + new_raw["operations"] = _decode_operation_strings(decoded) + return new_raw, True + + # If decoded is a dict with operations, unwrap it + if isinstance(decoded, dict) and "operations" in decoded: + inner_ops = decoded["operations"] + if isinstance(inner_ops, list): + new_raw = dict(raw) + new_raw["operations"] = _decode_operation_strings(inner_ops) + return new_raw, True + + # Any other decoded type - return unchanged + return raw, False + + # Case 3: operations is a list that might contain JSON-encoded strings + if isinstance(ops, list): + decoded_ops = _decode_operation_strings(ops) + if decoded_ops is not ops: # Identity check - was anything decoded? + new_raw = dict(raw) + new_raw["operations"] = decoded_ops + return new_raw, True + return raw, False + + return raw, False + + +def _decode_operation_strings(ops: list[Any]) -> list[Any]: + """Decode any JSON-encoded operation strings in a list. + + Args: + ops: List of operations, which may contain JSON-encoded strings. + + Returns: + List with JSON strings decoded to dicts. Returns the original list + if no decoding was needed (identity preserved). + """ + from hindsight_api.engine.llm_wrapper import parse_llm_json + + changed = False + result: list[Any] = [] + + for item in ops: + if isinstance(item, str): + try: + decoded = parse_llm_json(item) + if isinstance(decoded, dict): + result.append(decoded) + changed = True + else: + result.append(item) + except (json.JSONDecodeError, ValueError): + # Not valid JSON - keep as-is + result.append(item) + else: + result.append(item) + + return result if changed else ops + + +def _wrap_parse_delta_operation_list(original: Any) -> Any: + """Wrap parse_delta_operation_list to handle string-encoded operations.""" + + def wrapper(raw: Any) -> Any: + # Normalize the payload before passing to original + normalized, changed = _normalize(raw) + + if changed: + # Log at debug level when decoding was applied + ops = normalized.get("operations", []) if isinstance(normalized, dict) else [] + logger.debug( + "cerebral: decoded JSON-encoded delta operations (%d op(s))", + len(ops) if isinstance(ops, list) else 0, + ) + + # Call the original function with normalized payload + return original(normalized) + + # Mark the wrapper so we can detect double-installation + wrapper._codespy_delta_coercion = True # type: ignore[attr-defined] + return wrapper + + +def install_delta_ops_coercion() -> None: + """Install the delta operations coercion shim. + + This patches hindsight_api.engine.reflect.delta_ops.parse_delta_operation_list + to handle JSON-encoded operations strings before validation. + + Idempotent: calling multiple times has no additional effect. + """ + from hindsight_api.engine.reflect import delta_ops + + original = delta_ops.parse_delta_operation_list + + # Check if already installed + if getattr(original, "_codespy_delta_coercion", False): + return + + # Install the wrapper + delta_ops.parse_delta_operation_list = _wrap_parse_delta_operation_list(original) + logger.debug("cerebral: installed delta operations coercion shim") diff --git a/src/codespy/agents/memory/cerebral/orphans.py b/src/codespy/agents/memory/cerebral/orphans.py new file mode 100644 index 0000000..cff8b29 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/orphans.py @@ -0,0 +1,107 @@ +"""Orphaned async operation repair for Hindsight semantic memory. + +When SyncTaskBackend runs tasks inline, failures leave rows in `pending` status +because there's no worker poller to mark them failed or reschedule them. This +module provides startup repair to mark old pending operations as failed so that +deduplication no longer blocks new submissions. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from hindsight_api import MemoryEngine + +logger = logging.getLogger(__name__) + +# Operation types that can be orphaned by inline SyncTaskBackend execution +_ORPHAN_OPERATION_TYPES = ["refresh_mental_model", "consolidation"] + +# Default age threshold for considering an operation orphaned (1 hour) +_DEFAULT_OLDER_THAN_SECONDS = 3600 + +# Max error message length for the error_message column +_MAX_ERROR_MESSAGE_LENGTH = 5000 + + +async def repair_orphaned_operations( + engine: MemoryEngine, + schema: str, + bank_id: str, + older_than_s: int = _DEFAULT_OLDER_THAN_SECONDS, +) -> int: + """Mark orphaned pending operations as failed. + + Pending operations older than the threshold are marked failed with a + diagnostic message. This allows new submissions to proceed (the refresh + deduplication only blocks when a pending row exists). + + Args: + engine: The MemoryEngine instance. + schema: The schema name (e.g., "semantic"). + bank_id: The bank ID to repair operations for. + older_than_s: Age threshold in seconds (default 3600 = 1 hour). + + Returns: + Number of rows marked as failed. + """ + from hindsight_api.engine.db_utils import acquire_with_retry + + error_message = ( + "orphaned: left pending by inline SyncTaskBackend (repaired by codespy)" + ) + # Truncate if needed (though this message is well under the limit) + if len(error_message) > _MAX_ERROR_MESSAGE_LENGTH: + error_message = error_message[:_MAX_ERROR_MESSAGE_LENGTH] + + try: + async with acquire_with_retry(engine._backend, max_retries=1) as conn: + rows = await conn.fetch( + f""" + UPDATE "{schema}".async_operations + SET status = 'failed', + error_message = $2, + updated_at = NOW() + WHERE bank_id = $1 + AND status = 'pending' + AND operation_type = ANY($3::text[]) + AND updated_at < NOW() - MAKE_INTERVAL(SECS => $4) + RETURNING operation_id, operation_type + """, + bank_id, + error_message, + _ORPHAN_OPERATION_TYPES, + older_than_s, + ) + + if rows: + by_type: dict[str, int] = {} + for row in rows: + op_type = row["operation_type"] + by_type[op_type] = by_type.get(op_type, 0) + 1 + + logger.info( + "Repaired %d orphaned operation(s) in bank %r: %s", + len(rows), + bank_id, + ", ".join(f"{count} {op_type}" for op_type, count in by_type.items()), + ) + else: + logger.debug( + "No orphaned operations found in bank %r (older than %ds)", + bank_id, + older_than_s, + ) + + return len(rows) + + except Exception as exc: + logger.warning( + "Failed to repair orphaned operations in bank %r: %s", + bank_id, + exc, + exc_info=True, + ) + return 0 diff --git a/src/codespy/agents/memory/cerebral/routines.py b/src/codespy/agents/memory/cerebral/routines.py new file mode 100644 index 0000000..569314e --- /dev/null +++ b/src/codespy/agents/memory/cerebral/routines.py @@ -0,0 +1,230 @@ +"""Maintenance routine repair for Hindsight semantic memory. + +Upstream #2638: the maintenance routines (banks_needing_consolidation, +mental_models_with_cron, schemas_with_expired_rows, schemas_with_expired_operations) +install into and are called from `database_schema`. Cerebral sets its schema only +on the tenant extension, so `HINDSIGHT_API_DATABASE_SCHEMA` defaults to `public`, +but migrations run with `target_schema="semantic"`. With `database_schema=public` +and `target_schema=semantic`, the install migrations skip creating the routines, +and the maintenance loop warns that `public.() does not exist`. + +This module provides a one-time idempotent repair for existing databases. +Fresh databases get the routines via the normal migration path once the env +var is set (see cerebral.py). +""" + +from __future__ import annotations + +import glob +import importlib.util +import logging +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from hindsight_api import MemoryEngine + +logger = logging.getLogger(__name__) + +# The four maintenance routines that must exist in the configured schema +ROUTINE_NAMES = [ + "banks_needing_consolidation", + "schemas_with_expired_rows", + "mental_models_with_cron", + "schemas_with_expired_operations", +] + +# Upstream migration that defines the current routine bodies +REVISION_ID = "c8b4e2a71f95" + + +def _get_migration_path() -> Path | None: + """Find the migration file by revision ID in the hindsight_api package.""" + try: + import hindsight_api + except ImportError: + return None + + pkg_dir = Path(hindsight_api.__file__).parent + versions_dir = pkg_dir / "alembic" / "versions" + if not versions_dir.exists(): + return None + + pattern = str(versions_dir / f"{REVISION_ID}_*.py") + matches = glob.glob(pattern) + if len(matches) == 1: + return Path(matches[0]) + return None + + +def collect_routine_sql(schema: str) -> dict[str, str]: + """Extract SQL for all four maintenance routines from the upstream migration. + + Returns a mapping from routine name -> CREATE OR REPLACE FUNCTION SQL. + The SQL is qualified with the given schema (e.g., "semantic".routine_name). + + Raises RuntimeError if the migration file is missing or if the configured + schema does not match the requested schema (meaning HINDSIGHT_API_DATABASE_SCHEMA + was overridden to a different value). + """ + import hindsight_api + from hindsight_api.alembic._owned import externally_owned + + migration_path = _get_migration_path() + if migration_path is None: + raise RuntimeError( + f"Migration {REVISION_ID} not found in hindsight_api. " + "The repair cannot proceed." + ) + + # Load the migration module without registering in sys.modules permanently + module_name = f"_codespy_hs_{REVISION_ID}" + spec = importlib.util.spec_from_file_location(module_name, migration_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Failed to load spec for {migration_path}") + + module = importlib.util.module_from_spec(spec) + # Temporarily register to allow the loader to work + sys.modules[module_name] = module + try: + spec.loader.exec_module(module) + finally: + del sys.modules[module_name] + + # Prepare the collector that intercepts execute_unless_owned calls + captured: dict[str, str] = {} + + def collector(name: str, sql: str) -> None: + # Skip if externally owned (operator is managing this routine) + if externally_owned(name): + logger.info( + "Skipping routine %r: named in HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES", + name, + ) + return + captured[name] = sql + + # Mock the alembic context so _pg_upgrade can run without an active env + # The migration only uses context.config.get_main_option("target_schema") + module.context = SimpleNamespace( + config=SimpleNamespace( + get_main_option=lambda k: schema if k == "target_schema" else None + ) + ) + # Replace execute_unless_owned with our collector + module.execute_unless_owned = collector + + # Run the PostgreSQL upgrade function directly (not upgrade(), which needs op.get_bind) + module._pg_upgrade() + + # Validate results + if not captured: + # _pg_upgrade returned early because configured schema != target_schema + raise RuntimeError( + f"Migration {REVISION_ID} produced no SQL: configured database_schema " + f"does not match requested schema {schema!r}. " + "Is HINDSIGHT_API_DATABASE_SCHEMA set to a different value?" + ) + + # Verify all expected routines are present (unless externally owned) + missing = set(ROUTINE_NAMES) - set(captured.keys()) + if missing: + # Some routines may be externally owned, that's fine + for name in missing: + if externally_owned(name): + continue + raise RuntimeError( + f"Migration {REVISION_ID} did not produce SQL for routine {name!r}" + ) + + # Verify SQL contains the expected schema qualification + for name, sql in captured.items(): + expected = f'"{schema}".{name}' + if expected not in sql: + raise RuntimeError( + f"Routine {name!r} SQL does not contain expected qualification {expected!r}" + ) + + return captured + + +async def ensure_maintenance_routines(engine: MemoryEngine, schema: str) -> list[str]: + """Ensure all four maintenance routines exist in the given schema. + + Returns a list of routine names that were installed (may be empty if all + already exist). This is idempotent: running twice is a no-op on the second + run (the presence check returns all four). + + On any failure (migration file missing, permissions error, etc.), logs a + warning and returns an empty list. Cerebral stays usable; the maintenance + warnings will continue as the visible symptom. + """ + from hindsight_api.engine.db_utils import acquire_with_retry + + try: + sql_map = collect_routine_sql(schema) + except RuntimeError as exc: + logger.warning( + "Failed to collect maintenance routine SQL (revision %s): %s", + REVISION_ID, + exc, + exc_info=True, + ) + return [] + + # Check which routines are already present + try: + async with acquire_with_retry(engine._backend, max_retries=1) as conn: + rows = await conn.fetch( + """ + SELECT p.proname + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = $1 + AND p.proname = ANY($2::text[]) + """, + schema, + ROUTINE_NAMES, + ) + present = {row["proname"] for row in rows} + except Exception as exc: + logger.warning( + "Failed to check existing routines: %s", + exc, + exc_info=True, + ) + return [] + + missing = [name for name in ROUTINE_NAMES if name not in present and name in sql_map] + if not missing: + logger.debug("All maintenance routines already present in schema %r", schema) + return [] + + # Install missing routines in one transaction + installed: list[str] = [] + try: + async with acquire_with_retry(engine._backend, max_retries=1) as conn: + async with conn.transaction(): + for name in missing: + sql = sql_map[name] + await conn.execute(sql) + installed.append(name) + except Exception as exc: + logger.warning( + "Failed to install maintenance routines %s: %s", + missing, + exc, + exc_info=True, + ) + # Concurrent installs can fail with "tuple concurrently updated"; + # next boot's presence check will succeed if another process won. + return [] + + logger.info( + "Installed maintenance routines in schema %r: %s", + schema, + installed, + ) + return installed diff --git a/src/codespy/agents/memory/cerebral/structured_output.py b/src/codespy/agents/memory/cerebral/structured_output.py new file mode 100644 index 0000000..01f4f99 --- /dev/null +++ b/src/codespy/agents/memory/cerebral/structured_output.py @@ -0,0 +1,611 @@ +"""Structured output support for Cerebral memory models. + +Provides: +- Native structured output registration for Bedrock models that support it + but lack the flag in litellm's model map. +- JSON schema sanitization for Bedrock's structured output constraints. +- Two-step structured output fallback for models that don't enforce schemas. + +See: .kilo/plans/1790926015516-retain-facts-string-coercion.md +""" + +from __future__ import annotations + +import copy +import logging +from typing import Any, Callable + +logger = logging.getLogger(__name__) + +# Models that AWS documents as supporting native structured output on Bedrock, +# but which litellm's model map lacks the flag for. +# Format: bare_model_key -> litellm_provider (as passed to register_model) +# AWS model card: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-nvidia-nemotron-super-3-120b.html +_NATIVE_STRUCTURED_OUTPUT_OVERRIDES: dict[str, str] = { + "nvidia.nemotron-super-3-120b": "bedrock_converse", +} + +# Marker for idempotent wrapper installation +_SCHEMA_SANITIZER_MARKER = "_codespy_schema_sanitizer" + + +def register_native_structured_output_overrides() -> None: + """Register native structured output capability for known-capable models. + + Idempotent: safe to call multiple times. Skips keys where + supports_native_structured_output is already true. + """ + import litellm + from litellm.utils import supports_native_structured_output + + for bare_key, provider in _NATIVE_STRUCTURED_OUTPUT_OVERRIDES.items(): + try: + # Check if already registered + if supports_native_structured_output(bare_key, provider): + logger.debug( + "Native structured output already registered for %s (%s)", + bare_key, + provider, + ) + continue + except Exception: + # Model info not found - proceed with registration + pass + + # Register the capability + try: + litellm.register_model( + { + bare_key: { + "litellm_provider": provider, + "supports_native_structured_output": True, + } + } + ) + logger.debug( + "Registered native structured output for %s (%s)", bare_key, provider + ) + except Exception as e: + logger.warning( + "Failed to register native structured output for %s: %s", bare_key, e + ) + + # Verify registration for the primary model (with full litellm path prefix) + _verify_registration("bedrock/converse/nvidia.nemotron-super-3-120b") + + +def _verify_registration(model: str) -> None: + """Verify that native structured output is enabled for a model.""" + from litellm.utils import supports_native_structured_output + + try: + if supports_native_structured_output(model): + logger.debug("Verified native structured output for %s", model) + else: + logger.warning( + "Native structured output NOT enabled for %s after registration", model + ) + except Exception as e: + logger.warning("Could not verify native structured output for %s: %s", model, e) + + +def _strip_disallowed_keywords(schema: dict[str, Any]) -> dict[str, Any]: + """Recursively strip JSON Schema keywords disallowed by Bedrock. + + Bedrock's structured output accepts a subset of Draft 2020-12. + Disallowed keywords are silently rejected with a 400. + + Strips: + - minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf + - minLength, maxLength, pattern + - maxItems + - minItems with value > 1 (minItems: 0 or 1 is allowed) + - format values outside: date-time, time, date, duration, email, + hostname, uri, ipv4, ipv6, uuid + - additionalProperties values other than false (forced to false) + + Recurses through: properties, items, $defs, definitions, anyOf, allOf, oneOf. + Keys under properties/$defs are names, not keywords - never stripped. + + Returns a deep copy; input is never mutated. + """ + if not isinstance(schema, dict): + return schema + + # Deep copy to avoid mutating input + result = copy.deepcopy(schema) + return _strip_in_place(result) + + +def _strip_in_place(schema: Any) -> Any: + """In-place recursive strip. Internal helper.""" + if not isinstance(schema, dict): + return schema + + allowed_formats = { + "date-time", + "time", + "date", + "duration", + "email", + "hostname", + "uri", + "ipv4", + "ipv6", + "uuid", + } + + # Build list of keys to strip from this level + to_strip: list[str] = [] + modified: list[str] = [] + + for key, value in schema.items(): + if key in ( + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minLength", + "maxLength", + "pattern", + "maxItems", + ): + to_strip.append(key) + modified.append(key) + elif key == "minItems": + # Only strip if value > 1 + if isinstance(value, int) and value > 1: + to_strip.append(key) + modified.append(key) + elif key == "format": + if value not in allowed_formats: + to_strip.append(key) + modified.append(f"format:{value}") + elif key == "additionalProperties": + # Force to false if not already false + if value is not False: + schema[key] = False + modified.append(f"additionalProperties->{value}") + + # Strip disallowed keys + for key in to_strip: + del schema[key] + + # Log what was stripped at this level + if modified: + logger.debug("Bedrock schema sanitizer stripped: %s", modified) + + # Recurse into sub-schemas + # Keys under properties/$defs are names, not keywords - preserve them + for key in ("properties", "$defs", "definitions"): + if key in schema and isinstance(schema[key], dict): + for sub_schema in schema[key].values(): + _strip_in_place(sub_schema) + + # Array items + if "items" in schema: + _strip_in_place(schema["items"]) + + # Combiners + for combiner in ("anyOf", "allOf", "oneOf"): + if combiner in schema and isinstance(schema[combiner], list): + for sub_schema in schema[combiner]: + _strip_in_place(sub_schema) + + # AdditionalProperties (if it's a schema, not just false) + if isinstance(schema.get("additionalProperties"), dict): + _strip_in_place(schema["additionalProperties"]) + + # PropertyNames + if isinstance(schema.get("propertyNames"), dict): + _strip_in_place(schema["propertyNames"]) + + return schema + + +def sanitize_bedrock_json_schema(schema: dict[str, Any]) -> dict[str, Any]: + """Sanitize a JSON Schema for Bedrock's structured output constraints. + + Args: + schema: The JSON Schema dict to sanitize. + + Returns: + A deep copy with disallowed keywords stripped. + """ + return _strip_disallowed_keywords(schema) + + +def install_bedrock_schema_sanitizer() -> None: + """Install the Bedrock JSON schema sanitizer. + + Patches litellm's AmazonConverseConfig._create_output_config_for_response_format + to sanitize schemas before Bedrock receives them. + + Idempotent: calling multiple times has no additional effect. + """ + import functools + + try: + from litellm.llms.bedrock.chat.converse_transformation import ( + AmazonConverseConfig, + ) + except ImportError: + logger.debug("AmazonConverseConfig not found; skipping schema sanitizer") + return + + # Get the raw descriptor from the class __dict__ to handle staticmethod correctly + raw_descriptor = AmazonConverseConfig.__dict__.get( + "_create_output_config_for_response_format" + ) + if raw_descriptor is None: + logger.warning( + "AmazonConverseConfig._create_output_config_for_response_format not found" + ) + return + + # Unwrap staticmethod to get the actual function + if isinstance(raw_descriptor, staticmethod): + original = raw_descriptor.__func__ + else: + original = raw_descriptor + + # Check if already installed (check on the underlying function) + if getattr(original, _SCHEMA_SANITIZER_MARKER, False): + logger.debug("Bedrock schema sanitizer already installed") + return + + # The original signature is: (json_schema=None, name=None, description=None) + @functools.wraps(original) + def wrapper( + json_schema: dict[str, Any] | None = None, + name: str | None = None, + description: str | None = None, + ) -> Any: + """Wrapped staticmethod that sanitizes schemas before passing to Bedrock.""" + if json_schema is not None and isinstance(json_schema, dict): + # Deep copy and sanitize the json_schema + json_schema = _strip_disallowed_keywords(json_schema) + + return original(json_schema=json_schema, name=name, description=description) + + # Mark wrapper + setattr(wrapper, _SCHEMA_SANITIZER_MARKER, True) + + # Install wrapper as staticmethod + setattr( + AmazonConverseConfig, + "_create_output_config_for_response_format", + staticmethod(wrapper), + ) + logger.debug("Installed Bedrock JSON schema sanitizer") + + +def enforces_schema(model: str | None) -> bool | None: + """Check if a model enforces structured output schemas natively. + + Returns: + - True if the model natively enforces schemas + - False if it uses fallback tool-based extraction + - None if the model is unmapped (unknown to litellm) + """ + from litellm.utils import supports_native_structured_output, supports_response_schema + + if model is None: + return None + + # Parse provider from model string + # Format: "provider/model" or "provider/converse/model" etc. + parts = model.split("/") + provider = parts[0] if parts else None + + # Determine which litellm function to use + if provider in ("bedrock", "bedrock_converse"): + # Use native structured output flag for Bedrock + try: + return supports_native_structured_output(model) + except Exception: + return None + elif provider == "anthropic": + # Anthropic direct + try: + return supports_native_structured_output(model) + except Exception: + return None + elif provider == "vertex_ai": + # Vertex AI - check if it's a Claude model + if "claude" in model.lower(): + try: + return supports_native_structured_output(model) + except Exception: + return None + # Other Vertex models use response_schema + try: + return supports_response_schema(model) + except Exception: + return None + else: + # Other providers use response_schema + try: + return supports_response_schema(model) + except Exception: + return None + + +class TwoStepStructuredLLM: + """Two-step structured output wrapper for non-schema-enforcing LLMs. + + First call: ask the main LLM to produce JSON matching a schema (in the prompt). + Second call (if first fails validation): ask the extraction LLM to extract + structured data from the first response. + + Inner calls are costed via the existing span recorder. + """ + + def __init__( + self, + main_impl: Any, + extraction_impl: Any, + ): + """Initialize with main and extraction LLM implementations. + + Args: + main_impl: The main LiteLLMLLM to wrap. + extraction_impl: The extraction LiteLLMLLM for fallback. + """ + self._main = main_impl + self._extraction = extraction_impl + + def __getattr__(self, name: str) -> Any: + """Forward attribute access to the main LLM.""" + return getattr(self._main, name) + + async def call( + self, + messages: list[dict[str, Any]], + response_format: type | None = None, + **kwargs: Any, + ) -> Any: + """Call with two-step structured output handling. + + Args: + messages: The conversation messages. + response_format: A pydantic model class for structured output. + **kwargs: Additional args forwarded to inner calls. + + Returns: + LLMCallResult from either the main or extraction LLM. + """ + from hindsight_api.engine.llm_wrapper import ( + LLMCallResult, + OutputTooLongError, + parse_llm_json, + ) + from hindsight_api.engine.response_models import TokenUsage + from hindsight_api.engine.structured_output import provider_json_schema + + # If no response format, pass through directly (await the coroutine) + if response_format is None: + return await self._main.call(messages, response_format=None, **kwargs) + + # Check if response_format has a schema - use provider_json_schema for consistency + model_schema = None + try: + if hasattr(response_format, "model_json_schema"): + model_schema = provider_json_schema(response_format) + except Exception: + pass + + if model_schema is None: + # No schema available, pass through + return await self._main.call(messages, response_format=response_format, **kwargs) + + # Extract configuration values (forward all kwargs except ones we handle specially) + scope = kwargs.get("scope", "unknown") + skip_validation = kwargs.get("skip_validation", False) + strict_schema = kwargs.get("strict_schema", False) + + # Build step 1 messages with schema instruction + step1_messages = self._build_step1_messages(messages, model_schema) + + # Step 1: Call main LLM without response_format, with schema in prompt + # Forward all kwargs except response_format, skip_validation, strict_schema + step1_kwargs = {k: v for k, v in kwargs.items() if k not in ("response_format", "skip_validation", "strict_schema")} + step1_result = await self._main.call( + step1_messages, + response_format=None, + **step1_kwargs, + ) + + # LLMCallResult has `content`, not `text` + step1_content = step1_result.content + step1_text = step1_content if isinstance(step1_content, str) else str(step1_content) + + # Try to parse and validate step 1 result + try: + parsed = parse_llm_json(step1_text) + if skip_validation: + # Return raw dict as content + return LLMCallResult(content=parsed, usage=step1_result.usage) + else: + # Validate against response_format + validated = response_format.model_validate(parsed) + # Return validated model as content + return LLMCallResult(content=validated, usage=step1_result.usage) + except Exception: + # Step 1 failed validation or parsing - proceed to step 2 + pass + + # Step 2: Call extraction LLM + extraction_messages = self._build_extraction_messages(step1_text, model_schema) + + # Step 2 overrides temperature=0.0 and adds response_format, skip_validation, strict_schema + step2_kwargs = {k: v for k, v in kwargs.items() if k not in ("temperature", "response_format", "skip_validation", "strict_schema")} + step2_result = await self._extraction.call( + extraction_messages, + response_format=response_format, + temperature=0.0, + skip_validation=skip_validation, + strict_schema=strict_schema, + **step2_kwargs, + ) + + # Let OutputTooLongError propagate (handled by caller) + # Note: step2_result is LLMCallResult, not an exception + # OutputTooLongError would have been raised, not returned + + # Sum usage from both calls using TokenUsage.__add__ + combined_usage = step1_result.usage + step2_result.usage + + # Return step 2 result with combined usage + # Use model_copy (Pydantic v2) instead of _replace + return step2_result.model_copy(update={"usage": combined_usage}) + + def _build_step1_messages( + self, + messages: list[dict[str, Any]], + schema: dict[str, Any], + ) -> list[dict[str, Any]]: + """Build messages for step 1 with schema instruction appended to system.""" + import json + + messages_copy = list(messages) + + # Find system message or create one + system_idx = None + for i, msg in enumerate(messages_copy): + if msg.get("role") == "system": + system_idx = i + break + + schema_instruction = ( + f"\n\nRespond with a single JSON object matching this JSON schema:\n" + f"{json.dumps(schema, indent=2)}" + ) + + if system_idx is not None: + # Append to existing system message + original_content = messages_copy[system_idx].get("content", "") + messages_copy[system_idx] = { + **messages_copy[system_idx], + "content": original_content + schema_instruction, + } + else: + # Create new system message at the start + messages_copy.insert( + 0, {"role": "system", "content": schema_instruction.strip()} + ) + + return messages_copy + + def _build_extraction_messages( + self, + text: str, + schema: dict[str, Any], + ) -> list[dict[str, Any]]: + """Build messages for extraction step.""" + import json + + system_prompt = ( + f"Extract the structured data from the response below into this JSON schema. " + f"Copy values verbatim; do not invent, merge or drop items.\n\n" + f"{json.dumps(schema, indent=2)}" + ) + + return [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": text}, + ] + + # call_with_tools is forwarded via __getattr__ to self._main.call_with_tools + + +def install_two_step_structured_output( + engine: Any, + extraction_impl: Any, +) -> None: + """Install two-step structured output wrapper on eligible LLM configs. + + Wraps engine LLM configs whose main model doesn't enforce schemas but + the extraction model does. + + Args: + engine: The MemoryEngine instance. + extraction_impl: The extraction LLM implementation (LiteLLMLLM). + """ + # Correct import paths per plan fix B1 + from hindsight_api.engine.providers.litellm_llm import LiteLLMLLM + from hindsight_api.engine.multi_llm import MultiLLMProvider + + # Collect unique LLM configs from the engine + configs: set[int] = set() + llm_configs: list[Any] = [] + + for attr in ( + "_llm_config", + "_retain_llm_config", + "_consolidation_llm_config", + "_reflect_llm_config", + "_mental_model_refresh_llm_override", + ): + cfg = getattr(engine, attr, None) + if cfg is not None and id(cfg) not in configs: + configs.add(id(cfg)) + llm_configs.append(cfg) + + wrapped_count = 0 + for cfg in llm_configs: + # Check if the config itself is a MultiLLMProvider (fix B2) + # _build_llm returns MultiLLMProvider as the engine attribute + if isinstance(cfg, MultiLLMProvider): + logger.debug("Skipping MultiLLMProvider config for two-step wrapper") + continue + + impl = getattr(cfg, "_provider_impl", None) + + if impl is None: + logger.debug("No _provider_impl found on config") + continue + + # Skip if the impl is a MultiLLMProvider + if isinstance(impl, MultiLLMProvider): + logger.debug("Skipping MultiLLMProvider implementation for two-step wrapper") + continue + + if not isinstance(impl, LiteLLMLLM): + logger.debug("Skipping non-LiteLLMLLM implementation: %s", type(impl)) + continue + + # Get the main model + main_model = getattr(impl, "model", None) + extraction_model = getattr(extraction_impl, "model", None) + + # Check if wrapping is needed + main_enforces = enforces_schema(main_model) + extraction_enforces = enforces_schema(extraction_model) + + logger.debug( + "Checking two-step for model=%s (enforces=%s), extraction=%s (enforces=%s)", + main_model, + main_enforces, + extraction_model, + extraction_enforces, + ) + + # Wrap when main doesn't enforce but extraction does + if main_enforces is False and extraction_enforces is True: + wrapper = TwoStepStructuredLLM(impl, extraction_impl) + cfg._provider_impl = wrapper + wrapped_count += 1 + logger.info( + "Installed two-step structured output wrapper for %s (extraction=%s)", + main_model, + extraction_model, + ) + elif main_enforces is None: + logger.debug( + "Model %s is unmapped; skipping two-step wrapper", main_model + ) + + if wrapped_count > 0: + logger.info("Installed two-step structured output on %d LLM config(s)", wrapped_count) diff --git a/src/codespy/agents/memory/hippocampus/__init__.py b/src/codespy/agents/memory/hippocampus/__init__.py index bcdfbfa..226948c 100644 --- a/src/codespy/agents/memory/hippocampus/__init__.py +++ b/src/codespy/agents/memory/hippocampus/__init__.py @@ -3,6 +3,7 @@ CacheCandidate, ContextMemory, Mutation, + MutationType, Observation, ObservationTag, Operation, @@ -17,10 +18,7 @@ join_episode_saves, submit_episode_save, ) -from codespy.agents.memory.hippocampus.hippocampus import ( - Hippocampus, - inject_context_memory, -) +from codespy.agents.memory.hippocampus.hippocampus import Hippocampus from codespy.agents.memory.hippocampus.cartographer import Cartographer, CartographerSig from codespy.agents.memory.hippocampus.distiller import Distiller, DistillerSig @@ -35,6 +33,7 @@ "Hippocampus", "MemoryBudget", "Mutation", + "MutationType", "Observation", "ObservationTag", "Operation", @@ -42,7 +41,7 @@ "SectionName", "Topic", "compute_common_ancestor_topic_id", - "inject_context_memory", + "join_episode_saves", "make_topic_id", "submit_episode_save", diff --git a/src/codespy/agents/memory/hippocampus/budget.py b/src/codespy/agents/memory/hippocampus/budget.py index 85b56e4..40c1e32 100644 --- a/src/codespy/agents/memory/hippocampus/budget.py +++ b/src/codespy/agents/memory/hippocampus/budget.py @@ -26,14 +26,14 @@ class MemoryBudget: once (see ``Settings.get_memory_budget``) and shared across instances. Attributes: - max_context_memory_tokens: Hard ceiling on the serialized ContextMemory, + max_hippocampus_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 + most cost-sensitive of the four. Divided by ``max_hippocampus_item_tokens`` it 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 + max_hippocampus_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 verbose entry. Unlike the other three this is a **soft** budget: @@ -65,8 +65,8 @@ class MemoryBudget: input exceeds the model's context window. """ - max_context_memory_tokens: int = 16384 - max_context_item_tokens: int = 512 + max_hippocampus_tokens: int = 16384 + max_hippocampus_item_tokens: int = 512 max_trajectory_tokens: int | None = 16384 max_question_tokens: int | None = 8192 compact_trajectory: bool = True @@ -98,7 +98,7 @@ def count_tokens(s: str) -> int: def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: - """Serialize call inputs (excluding context_memory) for the Distiller question. + """Serialize call inputs for the Distiller question. All fields are included in full. If max_tokens is set, the joined result is head+tail bounded via _head_tail_text so both the instruction and any @@ -107,8 +107,6 @@ def format_inputs(kwargs: dict, max_tokens: int | None = None) -> str: """ parts: list[str] = [] for k, v in kwargs.items(): - if k == "context_memory": - continue parts.append(f"{k}: {v}") text = "\n".join(parts) if max_tokens is None: diff --git a/src/codespy/agents/memory/hippocampus/cartographer.py b/src/codespy/agents/memory/hippocampus/cartographer.py index adf88e8..3e88b2b 100644 --- a/src/codespy/agents/memory/hippocampus/cartographer.py +++ b/src/codespy/agents/memory/hippocampus/cartographer.py @@ -34,7 +34,7 @@ class CartographerSig(dspy.Signature): Prefer REPLACE over ADD when possible. - 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 + `max_hippocampus_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. @@ -123,7 +123,7 @@ class CartographerSig(dspy.Signature): question: str = dspy.InputField(desc="Question the agent was answering.") token_budget: int = dspy.InputField(desc="Hard token budget for the context memory.") current_tokens: int = dspy.InputField(desc="Current token count of the context memory.") - max_context_item_tokens: int = dspy.InputField( + max_hippocampus_item_tokens: int = dspy.InputField( desc="Token budget for a SINGLE context memory observation. Every ADD/REPLACE content " "must stay within it." ) @@ -147,8 +147,8 @@ class Cartographer(dspy.Module): enforcement is the Evictor's job. """ - # Name this module's settings live under: memory.cartographer. - SIGNATURE = "cartographer" + # Name this module's settings live under: memory.hippocampus.cartographer. + SIGNATURE = "memory_cartographer" def __init__(self): super().__init__() @@ -157,9 +157,9 @@ def __init__(self): self.predict = ContextSafe( dspy.ChainOfThought(CartographerSig), CartographerSig, - name="cartographer", - max_iters=settings.get_max_iters("cartographer"), - max_llm_calls=settings.get_max_llm_calls("cartographer"), + name=self.SIGNATURE, + max_iters=settings.get_max_iters(self.SIGNATURE), + max_llm_calls=settings.get_max_llm_calls(self.SIGNATURE), rlm_threshold=settings.get_rlm_threshold("chain_of_thought"), ) @@ -172,9 +172,9 @@ def forward( question, token_budget, current_tokens, - max_context_item_tokens, + max_hippocampus_item_tokens, ): - # See Distiller.forward: SignatureContext applies memory.cartographer's + # See Distiller.forward: SignatureContext applies memory.hippocampus.cartographer's # LLM settings and gives this module its own cost line. Entered here # because DSPy's context is thread-scoped and reflection runs in a worker. from codespy.agents import SignatureContext, get_cost_tracker @@ -188,5 +188,5 @@ def forward( question=question, token_budget=token_budget, current_tokens=current_tokens, - max_context_item_tokens=max_context_item_tokens, + max_hippocampus_item_tokens=max_hippocampus_item_tokens, ) diff --git a/src/codespy/agents/memory/hippocampus/context_memory.py b/src/codespy/agents/memory/hippocampus/context_memory.py index 91dbaed..306a20c 100644 --- a/src/codespy/agents/memory/hippocampus/context_memory.py +++ b/src/codespy/agents/memory/hippocampus/context_memory.py @@ -52,6 +52,38 @@ def _missing_(cls, value: object) -> OpType | None: return None +class MutationType(StrEnum): + """Recorded mutation types, persisted as ``observations.op_type``. + + A superset of :class:`OpType`: ``EVICT`` is recorded by Hippocampus (never + by the Cartographer) when a prior observation is dropped to stay within + ``max_hippocampus_tokens``. Both DELETE and EVICT write a tombstone + version; EVICT is a capacity decision, not evidence the fact is wrong. + """ + + ADD = "ADD" + DELETE = "DELETE" + REPLACE = "REPLACE" + EVICT = "EVICT" + + @classmethod + def _missing_(cls, value: object) -> MutationType | None: + if isinstance(value, str): + upper = value.upper() + for member in cls: + if member.value == upper: + return member + return None + + +# Mutation types that write a tombstone version (content NULL). Single source of +# truth: EpisodeStore.save_episode writes tombstones for these types and +# EpisodeStore.load_context excludes them. Add new tombstone types here only. +TOMBSTONE_TYPES: frozenset[MutationType] = frozenset( + {MutationType.DELETE, MutationType.EVICT} +) + + SectionName = Literal[ "context_roadmap", "context_understanding", @@ -114,7 +146,7 @@ class CacheCandidate(BaseModel): ), ) value: str = Field( - description="Compact candidate cache observation, within the max_context_item_tokens budget." + description="Compact candidate cache observation, within the max_hippocampus_item_tokens budget." ) transferability: str = Field( default="", @@ -139,21 +171,23 @@ class Operation(BaseModel): class Mutation(BaseModel): - """A recorded Cartographer mutation applied to the context memory. + """A recorded mutation applied to the context memory. - Tracks the sequence of ADD/DELETE/REPLACE operations with pre-mutation - state for debugging and audit purposes. + Tracks the sequence of ADD/DELETE/REPLACE operations (from the Cartographer) + and EVICT (from budget eviction) with pre-mutation state. """ step: int = Field(description="Which _distill() pass produced this mutation (0-indexed)") - type: OpType = Field(description="Type of mutation: ADD, DELETE, or REPLACE") - observation_id: str = Field(description="Generated ID (ADD) or existing ID (DELETE/REPLACE)") + type: MutationType = Field(description="Type of mutation: ADD, DELETE, REPLACE, or EVICT") + observation_id: str = Field( + description="Generated ID (ADD) or existing ID (DELETE/REPLACE/EVICT)" + ) section: SectionName = Field(description="Section the observation belongs to") content: str | None = Field( - default=None, description="New content (ADD/REPLACE); None for DELETE" + default=None, description="New content (ADD/REPLACE); None for DELETE/EVICT" ) previous_content: str | None = Field( - default=None, description="Old content (DELETE/REPLACE); None for ADD" + default=None, description="Old content (DELETE/REPLACE/EVICT); None for ADD" ) topic_ids: list[str] = Field( default_factory=list, description="Topic IDs associated with this mutation" diff --git a/src/codespy/agents/memory/hippocampus/distiller.py b/src/codespy/agents/memory/hippocampus/distiller.py index ccdfd57..053a203 100644 --- a/src/codespy/agents/memory/hippocampus/distiller.py +++ b/src/codespy/agents/memory/hippocampus/distiller.py @@ -38,8 +38,7 @@ class DistillerSig(dspy.Signature): ## Produce three outputs - 1. DIAGNOSIS — Brief (3-5 sentences; it feeds the next module's prompt, - so keep it terse) analysis of: + 1. DIAGNOSIS — Analysis of: - How many iterations the agent spent on orientation vs. question-specific work - Whether the agent re-discovered structural information that was @@ -108,7 +107,7 @@ class DistillerSig(dspy.Signature): Each candidate is an object with exactly these fields: - section: one of the six section names above - - value: the compact cached content (stay within max_context_item_tokens) + - value: the compact cached content (stay within max_hippocampus_item_tokens) - transferability: what kinds of future questions this would help - rationale: why this is shared understanding, not a one-off fact @@ -124,13 +123,13 @@ class DistillerSig(dspy.Signature): desc="Current context memory." ) question: str = dspy.InputField(desc="The question the agent was answering.") - max_context_item_tokens: int = dspy.InputField( + max_hippocampus_item_tokens: int = dspy.InputField( 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." ) diagnosis: str = dspy.OutputField( - desc="Brief (3-5 sentence) analysis of orientation vs. question-specific work, " + desc="Analysis of orientation vs. question-specific work, " "whether structural info was re-discovered that should have been cached, and " "what transferable understanding the agent built. Feeds the Cartographer prompt." ) @@ -139,7 +138,7 @@ class DistillerSig(dspy.Signature): "Keys must match existing observation ids exactly." ) cache_candidates: list[CacheCandidate] = dspy.OutputField( - desc="Candidate observations to add. Each within the max_context_item_tokens budget; " + desc="Candidate observations to add. Each within the max_hippocampus_item_tokens budget; " "structural/transferable only. " "Each candidate's `section` must be one of the six section names above." ) @@ -154,8 +153,8 @@ class Distiller(dspy.Module): specific work, tags every existing observation, and proposes new candidates. """ - # Name this module's settings live under: memory.distiller. - SIGNATURE = "distiller" + # Name this module's settings live under: memory.hippocampus.distiller. + SIGNATURE = "memory_distiller" def __init__(self): super().__init__() @@ -164,9 +163,9 @@ def __init__(self): self.predict = ContextSafe( dspy.ChainOfThought(DistillerSig), DistillerSig, - name="distiller", - max_iters=settings.get_max_iters("distiller"), - max_llm_calls=settings.get_max_llm_calls("distiller"), + name=self.SIGNATURE, + max_iters=settings.get_max_iters(self.SIGNATURE), + max_llm_calls=settings.get_max_llm_calls(self.SIGNATURE), rlm_threshold=settings.get_rlm_threshold("chain_of_thought"), ) @@ -175,9 +174,9 @@ def forward( trajectory: str, context_memory: ContextMemory, question: str, - max_context_item_tokens: int, + max_hippocampus_item_tokens: int, ): - # SignatureContext applies memory.distiller's model/temperature/reasoning + # SignatureContext applies memory.hippocampus.distiller's model/temperature/reasoning # effort and attributes the cost to this module rather than to whichever # agent triggered the reflection. It must be entered here, not by the # caller: Hippocampus reflects inside an asyncio.to_thread worker and @@ -189,5 +188,5 @@ def forward( trajectory=trajectory, context_memory=context_memory, question=question, - max_context_item_tokens=max_context_item_tokens, + max_hippocampus_item_tokens=max_hippocampus_item_tokens, ) diff --git a/src/codespy/agents/memory/hippocampus/episode.py b/src/codespy/agents/memory/hippocampus/episode.py index 2d7b061..05e257b 100644 --- a/src/codespy/agents/memory/hippocampus/episode.py +++ b/src/codespy/agents/memory/hippocampus/episode.py @@ -1,16 +1,27 @@ -"""Episode record and persistence helpers for Hippocampus.""" +"""Episode record and persistence helpers for Hippocampus. + +Background episode saves (retain + consolidation) are tracked as non-daemon threads. +The module registers an at-exit hook that runs BEFORE thread pool executors are +shut down, ensuring saves complete before the Python interpreter destroys the +executors. This prevents "cannot schedule new futures after shutdown" errors +from embedding calls during Cerebral consolidation. +""" from __future__ import annotations +import concurrent.futures.thread # noqa: F401 - ensures thread module loaded import logging import threading import uuid from collections.abc import Callable -from datetime import UTC, datetime +from contextlib import contextmanager +from datetime import datetime +from typing import Generator from pydantic import BaseModel, Field from codespy.agents.memory.hippocampus.context_memory import ContextMemory, Mutation +from codespy.agents.memory.recall import RecallRecord logger = logging.getLogger(__name__) @@ -25,42 +36,140 @@ _save_threads: list[threading.Thread] = [] _save_lock = threading.Lock() +# Deferred save queue: holds saves when _defer is True +_deferred: list[tuple[Callable[[], object], str]] = [] +_defer: bool = False + def submit_episode_save(target: Callable[[], object], *, name: str = "episode-save") -> None: """Start *target* in a non-daemon background thread and track it. - The thread is **non-daemon**: even without an explicit - ``join_episode_saves()`` call, the Python interpreter will wait for it - to finish before exiting. This guarantees episode persistence when the - main thread returns quickly (e.g. CLI publishes the review and exits). + The thread is **non-daemon**, so the Python interpreter will wait for it at + exit. However, non-daemon alone is not enough: atexit handlers run BEFORE + non-daemon threads are joined, and concurrent.futures registers a handler + that shuts down thread pools. Cerebral consolidation uses those pools for + embeddings, so if the main thread exits first, embedding calls fail with + "cannot schedule new futures after shutdown". + + To prevent this, episode.py registers its own at-exit hook via + threading._register_atexit(_join_episode_saves_at_exit). Hooks run in + reverse order, so this hook runs BEFORE the executor-shutdown hook. + Therefore, saves finish while the executors are still alive, even when + ReviewPipeline.forward() raised or the caller never joined explicitly. + + When _defer is True (inside defer_episode_saves() context), saves are + queued instead of started immediately. Call start_deferred_episode_saves() + to drain the queue and start them. Args: target: A zero-arg callable that performs the episode save. name: Thread name (for debugging / log messages). """ + with _save_lock: + if _defer: + _deferred.append((target, name)) + return + t = threading.Thread(target=target, name=name, daemon=False) with _save_lock: _save_threads.append(t) t.start() -def join_episode_saves(timeout_per_thread: float = 120) -> None: +@contextmanager +def defer_episode_saves() -> Generator[None, None, None]: + """Context manager that defers episode saves until exit. + + Inside this context, submit_episode_save() queues saves instead of + starting them. When the context exits, saves remain queued until + start_deferred_episode_saves() is called. + """ + global _defer + with _save_lock: + _defer = True + try: + yield + finally: + # _defer stays True - caller must call start_deferred_episode_saves() + pass + + +def start_deferred_episode_saves() -> None: + """Drain the deferred queue and start all queued saves. + + Called after the review phase completes to start the memory phase. + Saves submitted after this call start immediately (defer is off). + """ + global _defer, _deferred + with _save_lock: + _defer = False + to_start = list(_deferred) + _deferred.clear() + + for target, name in to_start: + t = threading.Thread(target=target, name=name, daemon=False) + with _save_lock: + _save_threads.append(t) + t.start() + if to_start: + logger.info("started %d deferred episode save(s)", len(to_start)) + + +def join_episode_saves(timeout_per_thread: float | None = None) -> None: """Block until every tracked episode-save thread has finished. Called once at the end of ``ReviewPipeline.forward()`` to guarantee episodes are persisted before the process may exit. Args: - timeout_per_thread: Max seconds to wait per thread (default 120). - If a thread exceeds the timeout it is abandoned with a warning. + timeout_per_thread: Max seconds to wait per thread (default None = wait + forever). If a timeout is given and a thread exceeds it, the thread + is abandoned with a warning. If the timeout is None, we wait + indefinitely for all saves to complete. """ - with _save_lock: - threads = list(_save_threads) - _save_threads.clear() - for t in threads: - t.join(timeout=timeout_per_thread) - if t.is_alive(): - logger.warning("Episode save thread %r did not finish in time", t.name) + while True: + with _save_lock: + # Take a snapshot and clear the registry for the next iteration + threads = list(_save_threads) + _save_threads.clear() + + if not threads: + # No more saves in flight + break + + # Log once if we're about to wait for any live threads + alive_count = sum(1 for t in threads if t.is_alive()) + if alive_count: + logger.info("waiting for %d background episode save(s)", alive_count) + + for t in threads: + t.join(timeout=timeout_per_thread) + if t.is_alive() and timeout_per_thread is not None: + # Only warn when a timeout was specified + logger.warning("Episode save thread %r did not finish in time", t.name) + + +def _join_episode_saves_at_exit() -> None: + """At-exit hook: join episode saves before thread pool executors shut down. + + Runs before concurrent.futures.thread._python_exit thanks to atexit hook + ordering (reverse registration order). This ensures Cerebral consolidations + that rely on ThreadPoolExecutor can still submit embedding tasks. + """ + try: + # Start any deferred saves that were never started + start_deferred_episode_saves() + join_episode_saves() + except BaseException: + # Log at debug level so Ctrl+C during exit doesn't print a traceback + logger.debug("Exception during episode save join at exit", exc_info=True) + + +# Register the at-exit hook to run BEFORE thread pool executors are shut down. +# This runs after concurrent.futures.thread is imported (at module top). +_register_atexit = getattr(threading, "_register_atexit", None) +if _register_atexit is not None: + _register_atexit(_join_episode_saves_at_exit) class Episode(BaseModel): @@ -93,6 +202,11 @@ class Episode(BaseModel): ``ReviewPipeline.forward()`` call, so all episodes from one review run can be correlated. mutations: Ordered sequence of Cartographer mutations applied during this episode. + recalls: Prefrontal recalls made during the agent call (pre-call load and + ``recall_memory`` tool calls), with the exact text the agent received, + model, token usage and cost. For monitoring only: persisted to the + ``recalls`` table, never given to the Distiller/Cartographer and never + retained into Cerebral. """ id: uuid.UUID = Field(description="Unique episode identifier (caller-provided)") @@ -123,3 +237,10 @@ class Episode(BaseModel): default_factory=list, description="Ordered sequence of Cartographer mutations applied during this episode", ) + recalls: list[RecallRecord] = Field( + default_factory=list, + description=( + "Prefrontal recalls of this agent call " + "(monitoring only; never distilled or retained)" + ), + ) diff --git a/src/codespy/agents/memory/hippocampus/hippocampus.py b/src/codespy/agents/memory/hippocampus/hippocampus.py index 0cdd599..14f0340 100644 --- a/src/codespy/agents/memory/hippocampus/hippocampus.py +++ b/src/codespy/agents/memory/hippocampus/hippocampus.py @@ -5,6 +5,7 @@ import asyncio import logging import uuid +from collections.abc import Sequence from datetime import UTC, datetime from typing import TYPE_CHECKING @@ -21,6 +22,7 @@ ContextMemory, ObservationTag, Mutation, + MutationType, Operation, OpType, Topic, @@ -32,54 +34,18 @@ if TYPE_CHECKING: from codespy.agents.memory.postgres import EpisodeStore + from codespy.agents.memory.recall import RecallRecord logger = logging.getLogger(__name__) -def prepend_context_memory(sig): - """Prepend context_memory field to signature.""" - return sig.prepend( - name="context_memory", - field=dspy.InputField( - desc="Current context memory. Use it before redundant tool calls." - ), - type_=ContextMemory, - ) - - -def inject_context_memory(module: dspy.Module) -> dspy.Module: - """Prepend context_memory input field to a dspy.Module's signatures. - - Idempotent — skips predictors that already have context_memory. - Mutates the module in place and returns it for chaining. - Works through ContextSafe's signature delegation. - """ - top_sig = getattr(module, "signature", None) - if top_sig is not None: - module_inputs = set(top_sig.input_fields) - if "context_memory" not in top_sig.input_fields: - module.signature = prepend_context_memory(top_sig) - for _, pred in module.named_predictors(): - if ( - set(pred.signature.input_fields) & module_inputs - and "context_memory" not in pred.signature.input_fields - ): - pred.signature = prepend_context_memory(pred.signature) - else: - for _, pred in module.named_predictors(): - if "context_memory" not in pred.signature.input_fields: - pred.signature = prepend_context_memory(pred.signature) - return module - - class Hippocampus: """Memory component that evolves via LLM-driven reflection. - The context memory is provided to agents so they start each run - with accumulated orientation knowledge (structure, entities, constants) about - the external context. After calls, the Distiller extracts transferable - understanding and the Cartographer edits the memory — "caching understanding, - not answers." + The context memory accumulates orientation knowledge (structure, entities, + constants) about the external context. After calls, the Distiller extracts + transferable understanding and the Cartographer edits the memory — + "caching understanding, not answers." ## Lifecycle @@ -88,11 +54,8 @@ class Hippocampus: # Construct Hippocampus with task name and optional memory budget hippo = Hippocampus(task_name="...") - # Inject context memory into agent - inject_context_memory(agent) - - # Run agent with context_memory input - pred = agent(context_memory=hippo.context_memory, task="…") + # Run agent (no memory input; agents will receive memory via Prefrontal later) + pred = agent(task="…") # Observe the result (buffers trajectory) hippo.observe(pred) @@ -104,7 +67,7 @@ class Hippocampus: hippo.end_episode(store, artifacts={"key": "value"}) # Async variant (for callers running inside an event loop) - pred = await agent.acall(context_memory=hippo.context_memory, task="…") + pred = await agent.acall(task="…") await hippo.aobserve(pred) await hippo.aend_episode(store, artifacts={"key": "value"}) @@ -146,7 +109,7 @@ def __init__( budget: The four token budgets bounding memory, as a :class:`MemoryBudget`. Defaults to ``MemoryBudget()`` — see that class for per-field guidance. Resolve one from configuration with - ``Settings.get_memory_budget(signature_name)``. + ``Settings.get_memory_budget()``. question: Pre-computed question string for the reflection "question". If set, this string is used directly as the Distiller question. If None, uses empty string (callers typically pass question at init). @@ -277,13 +240,19 @@ def _consolidate(self) -> str | None: ) return combined - def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: + def _finalize_episode( + self, + artifacts: dict[str, str] | None = None, + recalls: Sequence[RecallRecord] | None = None, + ) -> None: """Record the consolidated Episode snapshot and clear the buffer. Args: artifacts: Named output artifacts to attach to the recorded episode (e.g. ``{"review": ""}``). Defaults to an empty dict when omitted. + recalls: Prefrontal recall records to attach (monitoring only; + never passed to the Distiller/Cartographer). """ self.episode = Episode( id=uuid.uuid4(), @@ -294,7 +263,8 @@ def _finalize_episode(self, artifacts: dict[str, str] | None = None) -> None: timestamp=datetime.now(UTC), artifacts=artifacts or {}, run_id=self._run_id, - mutations=self._mutations, + mutations=list(self._mutations), + recalls=list(recalls or []), ) self._episode_trajectories.clear() self._episode_question = None @@ -305,6 +275,7 @@ def end_episode( self, store: EpisodeStore | None = None, artifacts: dict[str, str] | None = None, + recalls: Sequence[RecallRecord] | None = None, ) -> None: """Consolidate the buffered trajectories into the memory and record an Episode snapshot. @@ -326,6 +297,9 @@ def end_episode( episode (e.g. ``{"review": ""}``). Agent-agnostic — any caller can attach whatever markdown/text output it produced under a key of its choosing. + recalls: Prefrontal recall records of this agent call, stored on + the Episode and persisted to the ``recalls`` table. Never + given to the Distiller/Cartographer. Raises: OSError: If persistence is requested and the write fails. @@ -333,7 +307,7 @@ def end_episode( combined = self._consolidate() if combined is None: return - self._finalize_episode(artifacts) + self._finalize_episode(artifacts, recalls) if store is not None: store.save_episode(self.episode) @@ -341,6 +315,7 @@ async def aend_episode( self, store: EpisodeStore | None = None, artifacts: dict[str, str] | None = None, + recalls: Sequence[RecallRecord] | None = None, ) -> None: """Async counterpart of :meth:`end_episode`. @@ -352,11 +327,12 @@ async def aend_episode( store: Optional ``EpisodeStore`` to persist the episode after consolidation. artifacts: Named output artifacts to attach to the recorded episode (e.g. ``{"review": ""}``). + recalls: Prefrontal recall records (see :meth:`end_episode`). """ combined = await asyncio.to_thread(self._consolidate) if combined is None: return - await asyncio.to_thread(self._finalize_episode, artifacts) + await asyncio.to_thread(self._finalize_episode, artifacts, recalls) if store is not None: await asyncio.to_thread(store.save_episode, self.episode) @@ -398,7 +374,7 @@ def _record_mutations( mutations.append( Mutation( step=self._distill_step, - type=OpType.DELETE, + type=MutationType.DELETE, observation_id=op.observation_id, section=section, content=None, @@ -413,7 +389,7 @@ def _record_mutations( mutations.append( Mutation( step=self._distill_step, - type=OpType.REPLACE, + type=MutationType.REPLACE, observation_id=op.observation_id, section=section, content=op.content, @@ -429,7 +405,7 @@ def _record_mutations( if section_name: mut = Mutation( step=self._distill_step, - type=OpType.ADD, + type=MutationType.ADD, observation_id="", # back-filled from new_ids section=section_name, content=op.content, @@ -442,7 +418,7 @@ def _record_mutations( elif op.type == OpType.ADD and op.section and op.content: mut = Mutation( step=self._distill_step, - type=OpType.ADD, + type=MutationType.ADD, observation_id="", section=op.section, content=op.content, @@ -474,7 +450,7 @@ def _distill(self, trajectory: str, question: str) -> None: trajectory=trajectory, context_memory=self.cmem, question=question, - max_context_item_tokens=self.budget.max_context_item_tokens, + max_hippocampus_item_tokens=self.budget.max_hippocampus_item_tokens, ) known = self.cmem.ids() @@ -489,9 +465,9 @@ def _distill(self, trajectory: str, question: str) -> None: question=question, # The Cartographer's input field keeps the generic name: it is prompt # text, already scoped by its description, and pairs with current_tokens. - token_budget=self.budget.max_context_memory_tokens, + token_budget=self.budget.max_hippocampus_tokens, current_tokens=count_tokens(self.cmem.model_dump_json()), - max_context_item_tokens=self.budget.max_context_item_tokens, + max_hippocampus_item_tokens=self.budget.max_hippocampus_item_tokens, ) ops = list(edits.operations or []) @@ -503,8 +479,37 @@ def _distill(self, trajectory: str, question: str) -> None: for nid in new_ids: self.scores[nid] = self.scores.get(nid, 0) + 1 + pre_evict = self.cmem + self.cmem = evict(self.cmem, self.scores, self.budget.max_hippocampus_tokens) + self._record_evictions(pre_evict) self._distill_step += 1 - self.cmem = evict(self.cmem, self.scores, self.budget.max_context_memory_tokens) live = self.cmem.ids() self.scores = {k: v for k, v in self.scores.items() if k in live} + + def _record_evictions(self, pre_evict: ContextMemory) -> None: + """Record an EVICT mutation for every observation that eviction dropped. + + Records an EVICT for every observation no longer in context_memory, + including ones added this run. The log then states every eviction + explicitly, so Cerebral never has to infer one. + """ + post_ids = self.cmem.ids() + for obs in pre_evict.all_observations(): + if obs.id in post_ids: + continue + found = pre_evict.find_observation(obs.id) + if found is None: + continue + section, _ = found + self._mutations.append( + Mutation( + step=self._distill_step, + type=MutationType.EVICT, + observation_id=obs.id, + section=section, + content=None, + previous_content=obs.content, + topic_ids=list(obs.topic_ids), + ) + ) diff --git a/src/codespy/agents/memory/postgres.py b/src/codespy/agents/memory/postgres.py index c315002..802416e 100644 --- a/src/codespy/agents/memory/postgres.py +++ b/src/codespy/agents/memory/postgres.py @@ -4,21 +4,17 @@ 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.types.json import Jsonb from psycopg_pool import ConnectionPool if TYPE_CHECKING: - from codespy.agents.memory.hippocampus.context_memory import ( - ContextMemory, - Mutation, - Observation, - Topic, - ) + from codespy.agents.memory.hippocampus.context_memory import ContextMemory from codespy.agents.memory.hippocampus.episode import Episode + from codespy.agents.memory.recall import RecallRecord logger = logging.getLogger(__name__) @@ -86,6 +82,19 @@ def ensure_schema(self) -> None: # from any schema's search_path (episodic, semantic, etc.) cur.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA public") + # Ensure the semantic schema exists (used by Cerebral/MemoryEngine) + cur.execute("CREATE SCHEMA IF NOT EXISTS semantic") + + # pgvector extension — required by MemoryEngine for semantic search. + # Wrapped in a nested transaction (savepoint): pgvector may not + # be installed on all PostgreSQL instances; episodic memory + # works fine without it. + try: + with conn.transaction(): + cur.execute("CREATE EXTENSION IF NOT EXISTS vector SCHEMA public") + except Exception: + logger.debug("pgvector extension not available — semantic search requires it") + # Schema version table cur.execute(""" CREATE TABLE IF NOT EXISTS schema_version ( @@ -213,6 +222,53 @@ def ensure_schema(self) -> None: ) """) + # Check for legacy recalls table (without 'id' column) and drop it + cur.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'recalls' AND table_schema = COALESCE(current_schema(), 'public') + """) + existing_columns = {row[0] for row in cur.fetchall()} + if existing_columns and 'id' not in existing_columns: + logger.warning("Dropping legacy recalls table (missing 'id' column)") + cur.execute("DROP TABLE IF EXISTS recalls") + + # Recalls table: one row per Prefrontal recall (monitoring only; + # never read back into memory, never retained into Cerebral). + # Redesigned: run-scoped with optional episode_id (NULL for run-level load). + cur.execute(""" + CREATE TABLE IF NOT EXISTS recalls ( + bank_id VARCHAR(64) NOT NULL REFERENCES banks(id) ON DELETE CASCADE, + id UUID NOT NULL, + run_id VARCHAR(48) NOT NULL, + episode_id UUID NULL, + task VARCHAR(64) NOT NULL, + kind VARCHAR(8) NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + query TEXT NOT NULL DEFAULT '', + reach VARCHAR(8) NOT NULL, + reflects INT NOT NULL DEFAULT 0, + status VARCHAR(8) NOT NULL, + text TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', + llm_calls INT NOT NULL DEFAULT 0, + input_tokens INT NOT NULL DEFAULT 0, + output_tokens INT NOT NULL DEFAULT 0, + input_cost DOUBLE PRECISION NOT NULL DEFAULT 0, + output_cost DOUBLE PRECISION NOT NULL DEFAULT 0, + latency_ms INT NOT NULL DEFAULT 0, + details JSONB NOT NULL DEFAULT '{}'::jsonb, + PRIMARY KEY (bank_id, id), + FOREIGN KEY (bank_id, episode_id) REFERENCES episodes(bank_id, id) ON DELETE CASCADE + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_recalls_run ON recalls (bank_id, run_id) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_recalls_time + ON recalls (bank_id, timestamp DESC) + """) + conn.commit() def verify_access(self) -> None: @@ -231,11 +287,10 @@ def save_episode(self, episode: Episode) -> None: """ # Import here to avoid circular imports from codespy.agents.memory.hippocampus.context_memory import ( - ContextMemory, + TOMBSTONE_TYPES, Mutation, Observation, OpType, - Topic, ) with self._pool.connection() as conn: @@ -458,9 +513,12 @@ 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.observation_id: + # 6. Write tombstones (DELETE / EVICT) for observations not in + # current context. One per id: the id's last mutation decides. + # Skip when next_ver == 1: the id was never persisted, so no + # orphan tombstone row should be written. + for mutation in mutation_map.values(): + if mutation.type in TOMBSTONE_TYPES and mutation.observation_id: if mutation.observation_id not in processed_observation_ids: # Get next version number cur.execute( @@ -472,7 +530,11 @@ def save_episode(self, episode: Episode) -> None: (self.bank_id, mutation.observation_id), ) row = cur.fetchone() - version = row["next_ver"] if row else 1 + next_ver = row["next_ver"] if row else 1 + + # Skip if the id was never saved (next_ver == 1) + if next_ver == 1: + continue cur.execute( """ @@ -483,12 +545,12 @@ def save_episode(self, episode: Episode) -> None: ( self.bank_id, mutation.observation_id, - version, + next_ver, mutation.section, - None, # content is NULL for DELETE + None, # content is NULL for tombstones str(episode.id), mutation.step, - "DELETE", + mutation.type.value, # a TOMBSTONE_TYPES member mutation.previous_content, 0, ), @@ -505,13 +567,19 @@ def save_episode(self, episode: Episode) -> None: (self.bank_id, str(episode.id), name, content), ) + # 8. Insert recalls (Prefrontal monitoring) - now with episode_id set + self._insert_recalls( + cur, episode.recalls or [], run_id=episode.run_id, episode_id=episode.id, task=episode.task + ) + conn.commit() logger.info( - "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d)", + "save_episode: persisted episode %s (bank=%s, task=%s, topics=%d, observations=%d, recalls=%d)", episode.id, self.bank_id, episode.task, len(episode.context_memory.topics), len(episode.context_memory.all_observations()), + len(episode.recalls or []), ) def load_context( @@ -520,8 +588,10 @@ def load_context( topic_ids: list[str] | None = None, topic_prefix: str | None = None, ) -> ContextMemory | None: - """Load context for a new episode: latest observation versions from the - most recent episode for this (bank, task, topics). + """Load context for a new episode: latest observation versions from all episodes. + + Loads every observation from all prior episodes whose latest version + is not a tombstone (DELETE or EVICT) and bound to any of the specified topics. Args: task: Task name to filter by. @@ -530,165 +600,257 @@ def load_context( matches 'owner/repo', 'owner/repo/pkg', etc.). Mutually exclusive with topic_ids. - Returns ContextMemory (topics + observations + bindings) or None if - no prior episode exists. No mutations, artifacts, or episode - metadata are loaded. + Returns: + ContextMemory (topics + observations) or None if no filter provided + or no observations match. No mutations, artifacts, or episode + metadata are loaded. """ - try: - # Import here to avoid circular imports - from codespy.agents.memory.hippocampus.context_memory import ( - ContextMemory, - Observation, - Topic, - ) + # Import here to avoid circular imports + from codespy.agents.memory.hippocampus.context_memory import ( + TOMBSTONE_TYPES, + ContextMemory, + Observation, + Topic, + ) + + if not topic_ids and not topic_prefix: + logger.debug("load_context: no topic filter provided, returning None") + return None + + # Single source of truth for which op_types are tombstones. + tombstone_op_types = sorted(t.value for t in TOMBSTONE_TYPES) + try: 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 + # 1. Find observations whose latest version is not a tombstone + # and that are bound to matching topics params: list = [self.bank_id, task] if topic_ids: - filter_clause = sql.SQL("AND et.topic_id = ANY(%s)") - params.append(topic_ids) - elif topic_prefix: - filter_clause = sql.SQL("AND et.topic_id LIKE %s") - params.append(f"{topic_prefix}%") + filter_clause = sql.SQL("ot.topic_id = ANY(%s)") + params.append(list(topic_ids)) else: - filter_clause = sql.SQL("") - + # Escape LIKE wildcards so 'owner/my_repo' does not + # match 'owner/myXrepo/...'. Backslash is the default + # LIKE escape character in PostgreSQL. + escaped = ( + topic_prefix.replace("\\", "\\\\") + .replace("%", "\\%") + .replace("_", "\\_") + ) + filter_clause = sql.SQL("(ot.topic_id = %s OR ot.topic_id LIKE %s)") + params.extend([topic_prefix, f"{escaped}/%"]) + params.append(tombstone_op_types) + + # Query: get latest non-tombstone observation versions + # - Match topics via observation_topics on ANY version + # - Require version = MAX(version) over ALL versions of the id + # - Require the latest version to be live: op_type not in + # TOMBSTONE_TYPES, and content present. The content check + # also skips tombstones of a type this code does not know, + # instead of failing the whole load on a NULL content. + # - Order oldest-first by the timestamp of the episode that + # wrote the latest version. evict() drops the earliest + # positions first on ties, so the oldest facts are the + # ones evicted (and EVICT-tombstoned), not arbitrary ones. 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 {} - ORDER BY e.timestamp DESC - LIMIT 1 - """).format(filter_clause) + SELECT * FROM ( + SELECT DISTINCT ON (o.id) + o.id, + o.version, + o.type as section, + o.content, + o.episode_id, + e.timestamp + 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 {filter_clause} + ) + 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 <> ALL(%s) + AND o.content IS NOT NULL + ORDER BY o.id, o.version DESC + ) latest + ORDER BY latest.timestamp ASC, latest.id ASC + """).format(filter_clause=filter_clause) cur.execute(query, 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 + rows = cur.fetchall() - episode_id = row["id"] - logger.debug("load_context: found episode %s (bank=%s, task=%s)", episode_id, self.bank_id, task) + if not rows: + logger.debug("load_context: no observations found (bank=%s, task=%s)", self.bank_id, task) + return None - # 2. Load topics from that episode + # 2. Load topic bindings of exactly the versions loaded above cur.execute( """ - 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 + SELECT ot.observation_id, ot.topic_id + FROM observation_topics ot + JOIN unnest(%s::text[], %s::int[]) AS v(id, version) + ON v.id = ot.observation_id AND v.version = ot.observation_version + WHERE ot.bank_id = %s """, - (self.bank_id, episode_id), + ( + [row["id"] for row in rows], + [row["version"] for row in rows], + self.bank_id, + ), ) - topics = [ - Topic(id=row["id"], type=row["type"], description=row["description"]) - for row in cur.fetchall() - ] - - # 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 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' - """, - (self.bank_id, task, topic_id_list), - ) - observations_by_id: dict[str, dict] = {} + + obs_topics_map: dict[str, list[str]] = {} for row in cur.fetchall(): - observations_by_id[row["id"]] = { - "section": row["type"], - "content": row["content"], - "version": row["version"], - } - - # 4. Load observation-topic bindings for those latest versions - if observations_by_id: + obs_id = row["observation_id"] + if obs_id not in obs_topics_map: + obs_topics_map[obs_id] = [] + obs_topics_map[obs_id].append(row["topic_id"]) + + # 3. Build ContextMemory + ctx = ContextMemory(topics=[]) + + # Add observations to sections + for row in rows: + section = row["section"] + if section not in ctx.section_names(): + section = "context_understanding" # fallback + + obs = Observation( + id=row["id"], + content=row["content"], + topic_ids=obs_topics_map.get(row["id"], []), + ) + ctx.section(section).append(obs) + + # 4. Load Topics referenced by these observations + all_topic_ids = set() + for obs in ctx.all_observations(): + all_topic_ids.update(obs.topic_ids) + + if all_topic_ids: cur.execute( """ - 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 = 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' - ) + SELECT t.id, t.type, t.description + FROM topics t + WHERE t.bank_id = %s AND t.id = ANY(%s) """, - (self.bank_id, list(observations_by_id.keys())), + (self.bank_id, list(all_topic_ids)), ) - observation_topics_map: dict[str, list[str]] = {} for row in cur.fetchall(): - observation_id = row["observation_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[Observation]] = { - "context_roadmap": [], - "context_understanding": [], - "domain_constants": [], - "actions": [], - "parsing_schema": [], - "reusable_results": [], - } - - for observation_id, observation_data in observations_by_id.items(): - section = observation_data["section"] - if section not in sections: - section = "context_understanding" # fallback - - obs = Observation( - id=observation_id, - content=observation_data["content"], - topic_ids=observation_topics_map.get(observation_id, []), + ctx.topics.append( + Topic(id=row["id"], type=row["type"], description=row["description"]) ) - sections[section].append(obs) - - # Build ContextMemory - ctx = ContextMemory(topics=topics) - for section_name, observations in sections.items(): - setattr(ctx, section_name, observations) - 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) + total_observations = len(ctx.all_observations()) + logger.debug( + "load_context: loaded %d topics, %d observations (bank=%s, task=%s)", + len(ctx.topics), total_observations, self.bank_id, task + ) - return ctx + return ctx - # 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) + logger.warning( + "load_context failed (bank=%s, task=%s)", + self.bank_id, task, exc_info=True + ) return None + + def _insert_recalls( + self, + cur, + records: list[RecallRecord], + *, + run_id: str, + episode_id: uuid.UUID | None, + task: str, + ) -> None: + """Insert recall records with run_id, episode_id, and task filled in. + + Args: + cur: Database cursor + records: List of RecallRecord to insert + run_id: Run identifier + episode_id: Episode UUID (None for run-level recalls) + task: Task name (consumer task or "review" for run-level) + """ + import uuid as uuid_module + + for rec in records: + # Ensure the record has the correct run_id, episode_id, and task + rec.run_id = run_id + rec.task = task + cur.execute( + """ + INSERT INTO recalls + (bank_id, id, run_id, episode_id, task, kind, timestamp, query, reach, + reflects, status, text, model, llm_calls, input_tokens, + output_tokens, input_cost, output_cost, latency_ms, details) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (bank_id, id) DO NOTHING + """, + ( + self.bank_id, + str(rec.id) if rec.id else str(uuid_module.uuid4()), + rec.run_id, + str(episode_id) if episode_id else None, + rec.task, + rec.kind, + rec.timestamp, + rec.query, + rec.reach, + rec.reflects, + rec.status, + rec.text, + rec.model, + rec.llm_calls, + rec.input_tokens, + rec.output_tokens, + rec.input_cost, + rec.output_cost, + rec.latency_ms, + Jsonb(rec.details), + ), + ) + + def save_recalls( + self, + records: list[RecallRecord], + *, + run_id: str, + episode_id: uuid.UUID | None = None, + task: str = "", + ) -> None: + """Persist recall records for a run or episode. + + Args: + records: List of RecallRecord to persist + run_id: Run identifier + episode_id: Optional episode UUID (None for run-level recalls) + task: Task name (consumer task or "review" for run-level load) + """ + if not records: + return + + with self._pool.connection() as conn: + with conn.cursor() as cur: + # Ensure bank exists + cur.execute( + "INSERT INTO banks (id) VALUES (%s) ON CONFLICT (id) DO NOTHING", + (self.bank_id,), + ) + self._insert_recalls(cur, records, run_id=run_id, episode_id=episode_id, task=task) + conn.commit() + + logger.info( + "save_recalls: persisted %d recalls (bank=%s, run_id=%s, task=%s)", + len(records), self.bank_id, run_id, task, + ) diff --git a/src/codespy/agents/memory/prefrontal/__init__.py b/src/codespy/agents/memory/prefrontal/__init__.py new file mode 100644 index 0000000..50e3086 --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/__init__.py @@ -0,0 +1,46 @@ +"""Prefrontal – semantic recall from Cerebral into agent context.""" + +from codespy.agents.memory.prefrontal.facets import run_facets +from codespy.agents.memory.prefrontal.format import BeliefChange, FacetResult, format_context +from codespy.agents.memory.prefrontal.prefrontal import ( + MEMORY_UNAVAILABLE, + NO_RELEVANT_MEMORY, + PREFRONTAL_FIELD, + PREFRONTAL_FIELD_DESC, + RECALL_LIMIT_REACHED, + Prefrontal, + with_prefrontal_memory, +) +from codespy.agents.memory.prefrontal.query import Facet, build_facets +from codespy.agents.memory.prefrontal.reach import ( + REACHES, + build_local_groups, + build_remote_groups, + clamp_reach, + mental_model_id, + mental_model_ids, + org_of, +) + +__all__ = [ + "BeliefChange", + "Facet", + "FacetResult", + "MEMORY_UNAVAILABLE", + "NO_RELEVANT_MEMORY", + "PREFRONTAL_FIELD", + "PREFRONTAL_FIELD_DESC", + "Prefrontal", + "REACHES", + "RECALL_LIMIT_REACHED", + "build_facets", + "build_local_groups", + "build_remote_groups", + "clamp_reach", + "format_context", + "mental_model_id", + "mental_model_ids", + "org_of", + "run_facets", + "with_prefrontal_memory", +] diff --git a/src/codespy/agents/memory/prefrontal/facets.py b/src/codespy/agents/memory/prefrontal/facets.py new file mode 100644 index 0000000..2dd058f --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/facets.py @@ -0,0 +1,213 @@ +"""Run the pre-call facets against Cerebral.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Mapping, Sequence +from datetime import datetime +from typing import TYPE_CHECKING, Any + +from codespy.agents.memory.prefrontal.format import ( + BeliefChange, + FacetResult, + estimate_tokens, + fget, + render_change, + render_fact, +) +from codespy.agents.memory.prefrontal.query import ( + FACET_BELIEF_CHANGES, + FACET_CONTEXT, + FACET_DECISIONS, + FACET_PATTERNS, + FACET_SEEN_BEFORE, + Facet, +) + +if TYPE_CHECKING: + from codespy.agents.memory.cerebral import Cerebral + +logger = logging.getLogger(__name__) + +# belief_changes: observations taken from facets 2–4, and history entries kept per observation. +BELIEF_TOP_OBSERVATIONS = 3 +BELIEF_HISTORY_ENTRIES = 2 +BELIEF_SOURCE_FACETS: tuple[str, ...] = (FACET_SEEN_BEFORE, FACET_DECISIONS, FACET_PATTERNS) +CHANGE_MARKERS: tuple[str, ...] = ("RETRACTED", "supersedes:") + + +def facet_tokens(max_tokens: int, facet: Facet) -> int: + """Token budget of one facet: its fixed share of the pre-call budget.""" + return max(1, int(max_tokens * facet.share)) + + +async def _run_recall_facet( + cerebral: Cerebral, + facet: Facet, + *, + tag_groups: list[Any], + max_tokens: int, + budget: str, + question_date: datetime | None, + reflects: int, + reflect_context: str | None, +) -> FacetResult: + if not facet.query: + return FacetResult(facet.name) + tokens = facet_tokens(max_tokens, facet) + try: + if reflects > 0 and facet.name == FACET_CONTEXT: + text, summary = await cerebral.areflect( + facet.query, + tag_groups=tag_groups, + max_tokens=tokens, + context=reflect_context, + ) + return FacetResult(facet.name, text=text or "", reflect_summary=summary) + facts = await cerebral.arecall( + facet.query, + tag_groups=tag_groups, + max_tokens=tokens, + fact_type=list(facet.fact_types) if facet.fact_types else None, + prefer_observations=facet.prefer_observations, + budget=budget, + question_date=question_date, + ) + return FacetResult(facet.name, facts=list(facts or [])) + except Exception: + logger.warning("Prefrontal: facet %s failed", facet.name, exc_info=True) + return FacetResult(facet.name, failed=True) + + +def _is_change_fact(fact: Any) -> bool: + if fget(fact, "fact_type") != "world": + return False + text = fget(fact, "text") or "" + return any(marker in text for marker in CHANGE_MARKERS) + + +async def _belief_changes( + cerebral: Cerebral, + results: Mapping[str, FacetResult], + facet: Facet, + max_tokens: int, +) -> FacetResult: + """Facet 5: what did we believe before, and what changed? + + No recall and no embedding: reads the history of the top observations + of facets 2–4 and moves ``RETRACTED``/``supersedes:`` world facts out + of facets 1–4 into this section (so the merge's by-id dedup does not + drop them). Capped at the facet's share. + """ + out = FacetResult(facet.name) + budget = facet_tokens(max_tokens, facet) + + # Top observations from facets 2–4, by facet order then result order. + observations: list[Any] = [] + seen: set[str] = set() + for name in BELIEF_SOURCE_FACETS: + for fact in (results.get(name) or FacetResult(name)).facts: + fid = fget(fact, "id") + if fget(fact, "fact_type") == "observation" and fid and fid not in seen: + seen.add(fid) + observations.append(fact) + observations = observations[:BELIEF_TOP_OBSERVATIONS] + + async def _history(obs: Any) -> list[dict]: + try: + return await cerebral.aget_observation_history(fget(obs, "id")) + except Exception: + logger.debug( + "Prefrontal: observation history failed for %s", fget(obs, "id"), exc_info=True + ) + return [] + + histories = await asyncio.gather(*(_history(o) for o in observations)) + + used = 0 + full = False + for obs, history in zip(observations, histories, strict=True): + if full: + break + for i, entry in enumerate((history or [])[:BELIEF_HISTORY_ENTRIES]): + previous = (entry or {}).get("previous_text") + if not previous: + continue + changed = (entry or {}).get("changed_at") + change = BeliefChange( + id=f"history:{fget(obs, 'id')}:{i}", + current=fget(obs, "text") or "", + previous=previous, + changed_at=str(changed)[:10] if changed else None, + ) + cost = estimate_tokens(render_change(change)) + if used + cost > budget: + full = True + break + used += cost + out.changes.append(change) + + # Move RETRACTED / supersedes: world facts out of facets 1–4 (those that + # do not fit the share stay in their source facet). + for name in (FACET_CONTEXT, *BELIEF_SOURCE_FACETS): + source = results.get(name) + if source is None: + continue + kept: list[Any] = [] + for fact in source.facts: + if _is_change_fact(fact): + cost = estimate_tokens(render_fact(fact)) + if used + cost <= budget: + used += cost + out.facts.append(fact) + continue + kept.append(fact) + source.facts = kept + return out + + +async def run_facets( + cerebral: Cerebral, + facets: Sequence[Facet], + *, + tag_groups: list[Any], + max_tokens: int, + budget: str = "mid", + question_date: datetime | None = None, + reflects: int = 0, + reflect_context: str | None = None, +) -> dict[str, FacetResult]: + """Run facets 1–4 concurrently, then facet 5 from their observations. + + With ``reflects > 0`` only the ``context`` facet runs Hindsight's reflect + loop; the others stay raw recall. Each facet fails on its own and yields + an empty section. + """ + recall_facets = [f for f in facets if f.name != FACET_BELIEF_CHANGES] + gathered = await asyncio.gather( + *( + _run_recall_facet( + cerebral, + f, + tag_groups=tag_groups, + max_tokens=max_tokens, + budget=budget, + question_date=question_date, + reflects=reflects, + reflect_context=reflect_context, + ) + for f in recall_facets + ) + ) + results: dict[str, FacetResult] = {r.name: r for r in gathered} + belief = next((f for f in facets if f.name == FACET_BELIEF_CHANGES), None) + if belief is not None: + try: + results[FACET_BELIEF_CHANGES] = await _belief_changes( + cerebral, results, belief, max_tokens + ) + except Exception: + logger.warning("Prefrontal: facet %s failed", FACET_BELIEF_CHANGES, exc_info=True) + results[FACET_BELIEF_CHANGES] = FacetResult(FACET_BELIEF_CHANGES, failed=True) + return results diff --git a/src/codespy/agents/memory/prefrontal/format.py b/src/codespy/agents/memory/prefrontal/format.py new file mode 100644 index 0000000..91ac781 --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/format.py @@ -0,0 +1,224 @@ +"""Render Prefrontal context as one deterministic plain-text block.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any + +from codespy.agents.memory.prefrontal.query import ( + FACET_BELIEF_CHANGES, + FACET_CONTEXT, + FACET_DECISIONS, + FACET_ORDER, + FACET_PATTERNS, + FACET_SEEN_BEFORE, +) +from codespy.agents.memory.prefrontal.reach import ( + REACH_BANK, + TAG_REPO, + TAG_TASK, + org_of, + tag_value, +) + +# Content a mental model holds until its first refresh completes (kept in sync +# with cerebral.MENTAL_MODEL_PLACEHOLDER; not imported to avoid loading Hindsight). +MENTAL_MODEL_PLACEHOLDER = "Generating content..." + +SECTION_TITLES: dict[str, str] = { + FACET_CONTEXT: "Around this work", + FACET_SEEN_BEFORE: "Seen before", + FACET_DECISIONS: "Decisions", + FACET_PATTERNS: "Recurring patterns", + FACET_BELIEF_CHANGES: "What changed", +} + + +@dataclass(frozen=True) +class BeliefChange: + """One past wording of an observation.""" + + id: str + current: str + previous: str + changed_at: str | None = None + + +@dataclass +class FacetResult: + """Outcome of one facet. ``text`` holds a reflect answer (reflects > 0).""" + + name: str + facts: list[Any] = field(default_factory=list) + text: str = "" + changes: list[BeliefChange] = field(default_factory=list) + failed: bool = False + reflect_summary: Any = None # ReflectSummary from areflect, when reflects > 0 + + @property + def count(self) -> int: + return len(self.facts) + len(self.changes) + (1 if self.text else 0) + + +def estimate_tokens(text: str) -> int: + """Cheap token estimate (~4 chars per token), no tokenizer call.""" + return (len(text or "") + 3) // 4 + + +def fget(fact: Any, name: str, default: Any = None) -> Any: + """Read a field from a MemoryFact-like object or a dict.""" + if isinstance(fact, Mapping): + return fact.get(name, default) + return getattr(fact, name, default) + + +def _clean(text: str | None) -> str: + return " ".join((text or "").split()) + + +def fact_date(fact: Any) -> str | None: + """``YYYY-MM-DD`` of when the fact occurred, else when it was learned.""" + raw = fget(fact, "occurred_start") or fget(fact, "mentioned_at") + if not raw: + return None + return str(raw)[:10] + + +def render_fact(fact: Any, *, with_repo: bool = False) -> str: + """Render one fact as a bullet. + + - observation: ``- [observation] `` (``[repo · observation]`` for remote) + - otherwise: ``- [ · ] `` (``[repo · date · task]`` for remote) + """ + tags = fget(fact, "tags") or [] + parts: list[str] = [] + if with_repo: + repo = tag_value(tags, TAG_REPO) + if repo: + parts.append(repo) + if fget(fact, "fact_type") == "observation": + parts.append("observation") + else: + date = fact_date(fact) + if date: + parts.append(date) + task = tag_value(tags, TAG_TASK) + if task: + parts.append(task) + text = _clean(fget(fact, "text")) + label = f"[{' · '.join(parts)}] " if parts else "" + return f"- {label}{text}" + + +def render_change(change: BeliefChange) -> str: + """``- (was: , changed )``.""" + when = f", changed {change.changed_at}" if change.changed_at else "" + return f"- {_clean(change.current)} (was: {_clean(change.previous)}{when})" + + +class _Dedup: + """Keeps the first occurrence: by id, then by exact text.""" + + def __init__(self) -> None: + self.ids: set[str] = set() + self.texts: set[str] = set() + + def admit(self, key_id: str | None, text: str) -> bool: + text = _clean(text) + if key_id and key_id in self.ids: + return False + if text and text in self.texts: + return False + if key_id: + self.ids.add(key_id) + if text: + self.texts.add(text) + return True + + +def format_context_sections( + briefings: Sequence[Mapping[str, Any]], + facet_results: Mapping[str, FacetResult], + remote_facts: Sequence[Any], + repo: str, + reach: str = "org", +) -> list[tuple[str, str]]: + """Render briefings, facet sections (order 1 → 5) and remote facts as sections. + + Returns a list of (title, body) tuples in the current order. Empty sections + are omitted. The result can be joined by format_context to produce the same + output as before. + + Facts are deduplicated across sections by id, then by exact text, keeping + the first occurrence. + """ + sections: list[tuple[str, str]] = [] + + # Briefing sections + for mm in briefings or []: + content = (mm.get("content") or "").strip() + if not content or content == MENTAL_MODEL_PLACEHOLDER: + continue + name = (mm.get("name") or "Briefing").strip() + if not name.startswith("Briefing"): + name = f"Briefing: {name}" + sections.append((name, content)) + + dedup = _Dedup() + + # Facet sections in FACET_ORDER + for name in FACET_ORDER: + result = facet_results.get(name) + if result is None: + continue + lines: list[str] = [] + if result.text: + lines.append(result.text.strip()) + for fact in result.facts: + if dedup.admit(fget(fact, "id"), fget(fact, "text") or ""): + lines.append(render_fact(fact)) + for change in result.changes: + if dedup.admit(change.id, f"{change.current} (was: {change.previous})"): + lines.append(render_change(change)) + if lines: + sections.append((SECTION_TITLES[name], "\n".join(lines))) + + # Remote facts section + remote_lines = [ + render_fact(fact, with_repo=True) + for fact in remote_facts or [] + if dedup.admit(fget(fact, "id"), fget(fact, "text") or "") + ] + if remote_lines: + where = "" if reach == REACH_BANK else f" in {org_of(repo)}" + title = f"Other repositories{where} — verify before relying on it" + sections.append((title, "\n".join(remote_lines))) + + return sections + + +def format_context( + briefings: Sequence[Mapping[str, Any]], + facet_results: Mapping[str, FacetResult], + remote_facts: Sequence[Any], + repo: str, + reach: str = "org", +) -> str: + """Render briefings, facet sections (order 1 → 5) and remote facts. + + Facts are deduplicated across sections by id, then by exact text, keeping + the first occurrence. Empty sections are omitted; nothing at all → ``""``. + """ + sections = format_context_sections(briefings, facet_results, remote_facts, repo, reach) + return "\n\n".join(f"## {title}\n{body}" for title, body in sections) + + +def format_facts(facts: Sequence[Any], *, with_repo: bool = False) -> str: + """Render a flat, deduplicated bullet list (used by the recall tool).""" + dedup = _Dedup() + return "\n".join( + render_fact(f, with_repo=with_repo) + for f in facts or [] + if dedup.admit(fget(f, "id"), fget(f, "text") or "") + ) diff --git a/src/codespy/agents/memory/prefrontal/prefrontal.py b/src/codespy/agents/memory/prefrontal/prefrontal.py new file mode 100644 index 0000000..8b4bb75 --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/prefrontal.py @@ -0,0 +1,604 @@ +"""Prefrontal – hands useful prior knowledge from Cerebral to an agent. + +Reads Cerebral (the Hindsight bank) and never writes to it. Delivery is +hybrid: + +1. a pre-call context injected as the read-only ``prefrontal_memory`` input + (mental-model briefings, then five facets, then remote facts), and +2. a ``recall_memory`` tool the RLM agents can call for follow-ups. + +Prefrontal and Hippocampus never feed each other: the Distiller and the +Cartographer never see ``prefrontal_memory``. + +Every failure logs a warning and degrades to less context, or to ``""``. +""" + +from __future__ import annotations + +import asyncio +import concurrent.futures +import logging +import time +from collections.abc import Callable, Sequence +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any, Literal + +import dspy # type: ignore[import-untyped] + +from codespy.agents.memory.prefrontal.facets import run_facets +from codespy.agents.memory.prefrontal.format import ( + MENTAL_MODEL_PLACEHOLDER, + estimate_tokens, + fget, + format_context_sections, + format_facts, +) +from codespy.agents.memory.prefrontal.query import ( + FACET_BELIEF_CHANGES, + FACET_CONTEXT, + FACET_DECISIONS, + FACET_PATTERNS, + FACET_SEEN_BEFORE, + Facet, + truncate_words, +) +from codespy.agents.memory.prefrontal.reach import ( + REACH_LOCAL, + build_local_groups, + build_reflect_groups, + build_remote_groups, + clamp_reach, + is_own_repo_fact, + mental_model_ids, +) +from codespy.agents.memory.recall import RecallRecord, RecallUsage, track_recall_usage + +if TYPE_CHECKING: + from codespy.agents.memory.cerebral import Cerebral + from codespy.config_memory import PrefrontalConfig + +logger = logging.getLogger(__name__) + +PREFRONTAL_FIELD = "prefrontal_memory" +PREFRONTAL_FIELD_DESC = ( + "Prior knowledge from earlier runs (read-only, may be stale or wrong). " + "Use it to skip rediscovery and to direct tool calls. " + "Verify anything you report as an issue with tools; never report an issue from memory alone." +) + +RECALL_LIMIT_REACHED = "recall limit reached" +MEMORY_UNAVAILABLE = "memory unavailable" +NO_RELEVANT_MEMORY = "no relevant memory" +REMOTE_REFLECT_SUFFIX = ( + " For every fact that comes from another repository, name its source repository." +) +REMOTE_TOOL_HEADER = "Other repositories — verify before relying on it:" +# Max query chars in the per-recall INFO log line. +LOG_QUERY_CHARS = 120 + +RecallStatus = Literal["ok", "empty", "limit", "error"] + + +def _tool_status(text: str) -> RecallStatus: + """Classify a recall_memory result string.""" + if text == RECALL_LIMIT_REACHED: + return "limit" + if text == NO_RELEVANT_MEMORY: + return "empty" + if text == MEMORY_UNAVAILABLE: + return "error" + return "ok" + +_SIGNATURES: dict[type, type] = {} + + +def with_prefrontal_memory(signature: type[dspy.Signature]) -> type[dspy.Signature]: + """Return ``signature`` with a leading ``prefrontal_memory: str`` input (idempotent). + + Must be applied before the module is built: ``dspy.RLM`` builds its + predictors from the signature in ``__init__``. Use the same returned + signature for ``ContextSafe`` so its RLM fallback has the field too. + """ + if PREFRONTAL_FIELD in signature.input_fields: + return signature + cached = _SIGNATURES.get(signature) + if cached is not None: + return cached + injected = signature.prepend( + PREFRONTAL_FIELD, + dspy.InputField(desc=PREFRONTAL_FIELD_DESC), + type_=str, + ) + _SIGNATURES[signature] = injected + return injected + + +class Prefrontal: + """Per-agent-call reader of Cerebral. + + Args: + settings: ``memory.prefrontal`` config. + cerebral: The Cerebral instance to read. + task_name: Agent task (e.g. ``code_review``). + repo_full_name: ``owner/repo``. + scope_topic_ids: ``project_scope`` topic ids of the agent's scope(s). + include_repo: Also read repo-level facts (``repo:``) and briefing. + """ + + def __init__( + self, + settings: PrefrontalConfig, + cerebral: Cerebral, + task_name: str, + repo_full_name: str, + scope_topic_ids: Sequence[str] | None = None, + include_repo: bool = False, + ) -> None: + self._cfg = settings + self._cerebral = cerebral + self._task = task_name + self._repo = repo_full_name + self._scope_ids = [s for s in (scope_topic_ids or []) if s] + self._include_repo = include_repo + self._tool_calls = 0 + # One record per recall (pre-call load and each tool call), for monitoring. + # Kept across aload() calls: one Prefrontal serves one agent call. + self._records: list[RecallRecord] = [] + # Sections from the last aload() call, for structured memory rendering. + self._last_sections: list[tuple[str, str]] = [] + + @property + def recalls(self) -> list[RecallRecord]: + """Recall records so far (a copy). Persisted with the episode, never distilled.""" + return list(self._records) + + @property + def last_sections(self) -> list[tuple[str, str]]: + """Sections from the last aload() call as (title, body) tuples.""" + return list(self._last_sections) + + def _record( + self, + *, + kind: Literal["load", "tool"], + query: str, + reach: str, + started_at: datetime, + t0: float, + text: str, + status: RecallStatus, + usage: RecallUsage, + details: dict[str, Any], + ) -> RecallRecord | None: + """Append one RecallRecord. Never raises.""" + try: + rec = RecallRecord( + task=self._task, + kind=kind, + timestamp=started_at, + query=query, + reach=reach, + reflects=self._cfg.reflects, + status=status, + text=text, + model=usage.to_model_field(), + llm_calls=usage.llm_calls, + input_tokens=usage.input_tokens, + output_tokens=usage.output_tokens, + input_cost=usage.input_cost, + output_cost=usage.output_cost, + latency_ms=int((time.monotonic() - t0) * 1000), + details=details, + ) + self._records.append(rec) + return rec + except Exception: + logger.debug("Prefrontal[%s]: recall record failed", self._task, exc_info=True) + return None + + # Remote share constant: fraction of token budget for other repos when reach is org/bank + REMOTE_SHARE = 0.25 + # Recall budget constant: fixed at "mid" for raw recall + RECALL_BUDGET = "mid" + + @property + def reach(self) -> str: + return self._cfg.recall.reach + + @property + def reflects(self) -> int: + return self._cfg.reflects + + def _split_tokens(self, total: int, reach: str) -> tuple[int, int]: + """Split a token budget into (local, remote) by ``REMOTE_SHARE``. + + The remote share only applies when ``reach`` has a remote pool; otherwise + the local pool gets the whole budget. + """ + if build_remote_groups(self._repo, reach) is None: + return total, 0 + remote = int(total * self.REMOTE_SHARE) + return total - remote, remote + + def _reflect_context(self) -> str: + if len(self._scope_ids) == 1 and not self._include_repo: + target = self._scope_ids[0] + if not target.startswith(self._repo): + target = f"{self._repo}/{target}" + else: + target = self._repo + return f"{self._task} on {target}" + + # ------------------------------------------------------------------ + # Pre-call context + # ------------------------------------------------------------------ + + def load(self, facets: Sequence[Facet]) -> str: + """Blocking variant of :meth:`aload` (for the sync ChainOfThought agents).""" + try: + asyncio.get_running_loop() + except RuntimeError: + return asyncio.run(self.aload(facets)) + # A loop is running in this thread: run on a helper thread instead. + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(asyncio.run, self.aload(facets)).result() + + async def aload(self, facets: Sequence[Facet]) -> str: + """Build the pre-call context: briefings, facets 1 → 5, then remote facts. + + Records one ``kind="load"`` RecallRecord. Usage tracking is entered here, + inside the coroutine, so it also works for :meth:`load`, whose helper + thread does not copy the caller's context. + """ + self._tool_calls = 0 + self._last_sections = [] + facet_list = list(facets) + context_query = next((f.query for f in facet_list if f.name == FACET_CONTEXT), "") + started_at = datetime.now(UTC) + t0 = time.monotonic() + text = "" + status: RecallStatus = "error" + details: dict[str, Any] = {} + sections: list[tuple[str, str]] = [] + with track_recall_usage() as usage: + try: + text, details, sections = await self._aload(facet_list) + self._last_sections = sections + status = "ok" if text else "empty" + except Exception: + logger.warning("Prefrontal[%s]: load failed", self._task, exc_info=True) + text = "" + rec = self._record( + kind="load", + query=context_query, + reach=self._cfg.recall.reach, + started_at=started_at, + t0=t0, + text=text, + status=status, + usage=usage, + details=details, + ) + facet_counts = details.get("facets", {}) + + def _n(name: str) -> int: + return int((facet_counts.get(name) or {}).get("count", 0)) + + # Reflect signals for monitoring split synthesis and quality + reflect_info = details.get("reflect") or {} + maps = reflect_info.get("map_calls", 0) + rewrite = "rewrite" if reflect_info.get("rewrite") else "" + thoughts = reflect_info.get("usage", {}).get("thoughts_tokens", 0) + llm_calls = usage.llm_calls + expected_max = self._cfg.reflects + 1 # +1 for potential rewrite + + # Warn if split synthesis occurred (map_calls > 0) or calls exceeded expected max + if maps > 0: + logger.warning("Prefrontal[%s]: split synthesis detected (maps=%d)", self._task, maps) + if llm_calls > expected_max: + logger.warning( + "Prefrontal[%s]: reflect used %d LLM calls (> %d): maps=%d rewrite=%s tools=%s", + self._task, + llm_calls, + expected_max, + maps, + rewrite or "no", + ",".join(f"{name}:{size}" for name, size in reflect_info.get("tools", [])) or "-", + ) + + # Per-tool sizes for context calibration (e.g., "search_observations:1830,recall:2410") + tool_sizes = reflect_info.get("tools", []) + tools_str = ",".join(f"{name}:{size}" for name, size in tool_sizes) if tool_sizes else "-" + + logger.info( + "Prefrontal[%s]: load status=%s %d briefings, facets context=%d seen_before=%d " + "decisions=%d patterns=%d belief_changes=%d, remote=%d (%d tokens, reach=%s, " + "reflects=%d) model=%s calls=%d in=%d out=%d cost=$%.4f latency=%dms maps=%d%s thoughts=%d tools=%s", + self._task, + status, + int(details.get("briefings", 0)), + _n(FACET_CONTEXT), + _n(FACET_SEEN_BEFORE), + _n(FACET_DECISIONS), + _n(FACET_PATTERNS), + _n(FACET_BELIEF_CHANGES), + int(details.get("remote", 0)), + estimate_tokens(text), + self._cfg.recall.reach, + self._cfg.reflects, + usage.to_model_field() or "-", + llm_calls, + usage.input_tokens, + usage.output_tokens, + usage.input_cost + usage.output_cost, + rec.latency_ms if rec else int((time.monotonic() - t0) * 1000), + maps, + f" {rewrite}" if rewrite else "", + thoughts, + tools_str, + ) + return text + + async def _aload(self, facets: list[Facet]) -> tuple[str, dict[str, Any], list[tuple[str, str]]]: + """Return the rendered context, load ``details``, and sections for the RecallRecord.""" + cfg = self._cfg + now = datetime.now(UTC) + local_groups = build_local_groups(self._repo, self._scope_ids, self._include_repo) + context_query = next((f.query for f in facets if f.name == FACET_CONTEXT), "") + local_tokens, remote_tokens = self._split_tokens( + cfg.recall.max_tokens, cfg.recall.reach + ) + + async def _briefings() -> list[dict]: + if cfg.reflects <= 0: + return [] + ids = mental_model_ids(self._repo, self._scope_ids, self._include_repo) + if not ids: + return [] + try: + return await self._cerebral.aget_mental_models(ids) + except Exception: + logger.warning("Prefrontal[%s]: briefings failed", self._task, exc_info=True) + return [] + + async def _facets() -> dict: + if not local_groups: + return {} + return await run_facets( + self._cerebral, + facets, + tag_groups=local_groups, + max_tokens=local_tokens, + budget="mid", + question_date=now, + reflects=cfg.reflects, + reflect_context=self._reflect_context(), + ) + + def _extract_reflect_summary(results: dict) -> dict | None: + """Extract reflect summary from context facet for monitoring.""" + from codespy.agents.memory.prefrontal.query import FACET_CONTEXT + + ctx = results.get(FACET_CONTEXT) + if ctx is None or ctx.reflect_summary is None: + return None + s = ctx.reflect_summary + return { + "iterations": s.iterations, + "llm_calls": s.llm_calls, + "map_calls": s.map_calls, + "rewrite": s.rewrite, + "tools": s.tools, + "usage": s.usage, + "empty": s.empty, + } + + async def _remote() -> list[Any]: + groups = build_remote_groups(self._repo, cfg.recall.reach) + if groups is None or remote_tokens <= 0 or not context_query: + return [] + try: + facts = await self._cerebral.arecall( + context_query, + tag_groups=groups, + max_tokens=remote_tokens, + fact_type=["world", "observation"], + prefer_observations=True, + budget=self.RECALL_BUDGET, + question_date=now, + ) + except Exception: + logger.warning("Prefrontal[%s]: remote recall failed", self._task, exc_info=True) + return [] + return [f for f in facts or [] if not is_own_repo_fact(fget(f, "tags"), self._repo)] + + briefings, results, remote = await asyncio.gather(_briefings(), _facets(), _remote()) + # Compute sections once for structured rendering + sections = format_context_sections( + briefings, results, remote, self._repo, cfg.recall.reach + ) + text = "\n\n".join(f"## {t}\n{b}" for t, b in sections) + + n_briefings = sum( + 1 + for mm in briefings + if (mm.get("content") or "").strip() not in ("", MENTAL_MODEL_PLACEHOLDER) + ) + facet_details: dict[str, dict[str, Any]] = {} + for f in facets: + r = results.get(f.name) + facet_details[f.name] = { + "query": f.query, + "count": r.count if r is not None else 0, + "failed": bool(r.failed) if r is not None else False, + } + details: dict[str, Any] = { + "briefings": n_briefings, + "remote": len(remote), + "facets": facet_details, + "reflect": _extract_reflect_summary(results), + } + return text, details, sections + + # ------------------------------------------------------------------ + # Tool + # ------------------------------------------------------------------ + + def recall_tool(self) -> Callable[..., Any] | None: + """Async ``recall_memory`` tool for RLM agents, or None when disabled.""" + if self._cfg.recall.max_tool_calls <= 0: + return None + + async def recall_memory(query: str, reach: str = "local") -> str: + """Search prior knowledge learned in earlier runs (read-only, may be stale or wrong). + + Args: + query: What to look up, e.g. a file, symbol, package, problem or decision. + reach: "local" (this scope/repository), "org" (also other repositories + of the same owner) or "bank" (all repositories). Clamped to the + configured reach. + + Returns: + Matching facts as text. Verify with tools before reporting any issue. + """ + return await self._recall(query, reach) + + return recall_memory + + async def _recall(self, query: str, reach: str = "local") -> str: + """Run one recall_memory call and record it (every exit path, exactly once).""" + cfg = self._cfg + started_at = datetime.now(UTC) + t0 = time.monotonic() + # str(): RLM agents may pass non-string arguments; this must never raise. + q = truncate_words(str(query or "")) + effective = clamp_reach(str(reach or ""), cfg.recall.reach) + details: dict[str, Any] = { + "requested_reach": str(reach or ""), + "mode": "reflect" if cfg.reflects > 0 else "recall", + } + with track_recall_usage() as usage: + if self._tool_calls >= cfg.recall.max_tool_calls: + text = RECALL_LIMIT_REACHED + else: + self._tool_calls += 1 + text = await self._recall_impl(q, effective, details) + status = _tool_status(text) + rec = self._record( + kind="tool", + query=q, + reach=effective, + started_at=started_at, + t0=t0, + text=text, + status=status, + usage=usage, + details=details, + ) + logger.info( + "Prefrontal[%s]: recall_memory reach=%s status=%s mode=%s model=%s calls=%d " + "in=%d out=%d cost=$%.4f latency=%dms query=%r", + self._task, + effective, + status, + details["mode"], + usage.to_model_field() or "-", + usage.llm_calls, + usage.input_tokens, + usage.output_tokens, + usage.input_cost + usage.output_cost, + rec.latency_ms if rec else int((time.monotonic() - t0) * 1000), + q[:LOG_QUERY_CHARS], + ) + return text + + async def _recall_impl(self, q: str, effective: str, details: dict[str, Any]) -> str: + """recall_memory body. Fills fact counts into ``details`` (recall mode).""" + cfg = self._cfg + try: + if not q: + return NO_RELEVANT_MEMORY + now = datetime.now(UTC) + + if cfg.reflects > 0: + groups = build_reflect_groups( + self._repo, self._scope_ids, self._include_repo, effective + ) + if groups == []: + return NO_RELEVANT_MEMORY + prompt = q if effective == REACH_LOCAL else q + REMOTE_REFLECT_SUFFIX + text, summary = await self._cerebral.areflect( + prompt, + tag_groups=groups, + max_tokens=cfg.recall.max_tool_tokens, + context=self._reflect_context(), + ) + details["reflect"] = { + "iterations": summary.iterations, + "llm_calls": summary.llm_calls, + "map_calls": summary.map_calls, + "rewrite": summary.rewrite, + "tools": summary.tools, + "usage": summary.usage, + "empty": summary.empty, + } + # Warn if calls exceeded expected max (reflects + 1 for potential rewrite) + expected_max = cfg.reflects + 1 + if summary.llm_calls > expected_max: + logger.warning( + "Prefrontal[%s]: reflect used %d LLM calls (> %d): maps=%d rewrite=%s tools=%s", + self._task, + summary.llm_calls, + expected_max, + summary.map_calls, + "yes" if summary.rewrite else "no", + ",".join(f"{name}:{size}" for name, size in summary.tools) or "-", + ) + # Warn if split synthesis occurred + if summary.map_calls > 0: + logger.warning( + "Prefrontal[%s]: split synthesis detected (maps=%d)", + self._task, + summary.map_calls, + ) + return text or NO_RELEVANT_MEMORY + + parts: list[str] = [] + local_tokens, remote_tokens = self._split_tokens( + cfg.recall.max_tool_tokens, effective + ) + local_groups = build_local_groups(self._repo, self._scope_ids, self._include_repo) + if local_groups: + facts = await self._cerebral.arecall( + q, + tag_groups=local_groups, + max_tokens=local_tokens, + prefer_observations=True, + budget=self.RECALL_BUDGET, + question_date=now, + ) + details["local"] = len(facts or []) + parts.append(format_facts(facts)) + remote_groups = build_remote_groups(self._repo, effective) + if remote_groups is not None and remote_tokens > 0: + facts = await self._cerebral.arecall( + q, + tag_groups=remote_groups, + max_tokens=remote_tokens, + fact_type=["world", "observation"], + prefer_observations=True, + budget=self.RECALL_BUDGET, + question_date=now, + ) + facts = [ + f for f in facts or [] if not is_own_repo_fact(fget(f, "tags"), self._repo) + ] + details["remote"] = len(facts) + rendered = format_facts(facts, with_repo=True) + if rendered: + parts.append(f"{REMOTE_TOOL_HEADER}\n{rendered}") + text = "\n\n".join(p for p in parts if p) + return text or NO_RELEVANT_MEMORY + except Exception: + logger.warning("Prefrontal[%s]: recall_memory failed", self._task, exc_info=True) + return MEMORY_UNAVAILABLE diff --git a/src/codespy/agents/memory/prefrontal/query.py b/src/codespy/agents/memory/prefrontal/query.py new file mode 100644 index 0000000..84d7ccb --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/query.py @@ -0,0 +1,161 @@ +"""Pre-call facet queries: five question-shaped recalls over the local pool. + +Mental models (briefings) are the abstraction layer ("when X happens, +consider Y because Z"). The facets are the semantic layer: they answer the +messier questions a briefing cannot, so they always run, even when +briefings exist. + +None of these queries is the Hippocampus ``question``. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass + +# Facet names, in merge/render order (1 → 5). +FACET_CONTEXT = "context" +FACET_SEEN_BEFORE = "seen_before" +FACET_DECISIONS = "decisions" +FACET_PATTERNS = "patterns" +FACET_BELIEF_CHANGES = "belief_changes" +FACET_ORDER: tuple[str, ...] = ( + FACET_CONTEXT, + FACET_SEEN_BEFORE, + FACET_DECISIONS, + FACET_PATTERNS, + FACET_BELIEF_CHANGES, +) + +# Share of memory.prefrontal.recall.max_tokens per facet (sums to 1.0). +SHARE_CONTEXT = 0.30 +SHARE_SEEN_BEFORE = 0.20 +SHARE_DECISIONS = 0.20 +SHARE_PATTERNS = 0.20 +SHARE_BELIEF_CHANGES = 0.10 + +# Query caps. +MAX_QUERY_WORDS = 120 +MAX_SUMMARY_CHARS = 400 +MAX_CONTEXT_PATHS = 8 +MAX_ENTITY_TERMS = 12 + + +@dataclass(frozen=True) +class Facet: + """One pre-call recall. + + ``fact_types`` is None and ``query`` empty for ``belief_changes``, which + is derived from the other facets' results (no recall). + """ + + name: str + query: str + fact_types: tuple[str, ...] | None + prefer_observations: bool + share: float + + +def truncate_chars(text: str, limit: int) -> str: + """Cut ``text`` to at most ``limit`` chars, at a word boundary.""" + text = " ".join((text or "").split()) + if len(text) <= limit: + return text + cut = text[: limit + 1] + space = cut.rfind(" ") + return (cut[:space] if space > 0 else text[:limit]).rstrip(" ,;:.") + + +def truncate_words(text: str, limit: int = MAX_QUERY_WORDS) -> str: + """Keep at most ``limit`` whitespace-separated words.""" + words = (text or "").split() + return " ".join(words[:limit]) + + +def _join(parts: Sequence[str], sep: str) -> str: + """Join non-empty parts; with a sentence separator, drop each part's final period.""" + strip = sep.strip() + cleaned = (s.strip() for s in parts) + if strip: + cleaned = (s.rstrip(strip).rstrip() for s in cleaned) + return sep.join(p for p in cleaned if p) + + +def _dedup(items: Sequence[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for item in items: + item = (item or "").strip() + if item and item not in seen: + seen.add(item) + out.append(item) + return out + + +def build_facets( + task: str, + target: str, + description: str = "", + pr_title: str = "", + paths: Sequence[str] = (), + symbols: Sequence[str] = (), + summary: str = "", + packages: Sequence[str] = (), +) -> list[Facet]: + """Build the five pre-call facets for one agent call. + + Args: + task: Agent task name (e.g. ``code_review``). + target: Scope topic id, or the repo for repo-level agents. + description: Scope description (empty for repo-level agents). + pr_title: Pull request title. + paths: Changed file paths. + symbols: Changed function/class names, when already known. + summary: PR summary (or truncated PR description); capped at 400 chars. + packages: Package names of the scope (manifest identity). + """ + paths = _dedup(paths) + summary = truncate_chars(summary, MAX_SUMMARY_CHARS) + head = _join([task, target], " ") + if description: + head = f"{head} — {truncate_chars(description, MAX_SUMMARY_CHARS)}" + + context_q = _join( + [ + head, + f"PR: {pr_title.strip()}" if pr_title and pr_title.strip() else "", + summary, + f"Files: {', '.join(paths[:MAX_CONTEXT_PATHS])}" if paths else "", + ], + ". ", + ) + entities = _dedup([*paths, *symbols])[:MAX_ENTITY_TERMS] + seen_q = _join([target, *_dedup(packages), *entities], " ") + decisions = ( + "decisions, trade-offs, chosen approaches, rejected alternatives, agreed conventions" + ) + patterns = "recurring issues, repeated findings, patterns across changes" + decisions_q = f"{target}: {decisions}" if target else decisions + patterns_q = f"{target}: {patterns}" if target else patterns + + return [ + Facet( + FACET_CONTEXT, truncate_words(context_q), ("experience", "world"), False, SHARE_CONTEXT + ), + Facet( + FACET_SEEN_BEFORE, + truncate_words(seen_q), + ("world", "observation"), + True, + SHARE_SEEN_BEFORE, + ), + Facet( + FACET_DECISIONS, + truncate_words(decisions_q), + ("world", "observation"), + True, + SHARE_DECISIONS, + ), + Facet(FACET_PATTERNS, truncate_words(patterns_q), ("observation",), False, SHARE_PATTERNS), + Facet(FACET_BELIEF_CHANGES, "", None, False, SHARE_BELIEF_CHANGES), + ] diff --git a/src/codespy/agents/memory/prefrontal/reach.py b/src/codespy/agents/memory/prefrontal/reach.py new file mode 100644 index 0000000..eb806c5 --- /dev/null +++ b/src/codespy/agents/memory/prefrontal/reach.py @@ -0,0 +1,183 @@ +"""Tag scopes and reach filters shared by Cerebral (write side) and Prefrontal (read side). + +This module is deliberately lightweight: it never imports ``hindsight_api`` at +module level. Hindsight builds and caches its config at import time, and +``get_cerebral`` must set ``HINDSIGHT_API_*`` env vars before that happens, so +the Hindsight tag-group classes are imported lazily inside the functions that +build them (only ever called once a Cerebral exists). +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Iterable, Sequence +from typing import Any + +# Reach levels, from narrowest to widest. +REACH_LOCAL = "local" +REACH_ORG = "org" +REACH_BANK = "bank" +REACHES: tuple[str, ...] = (REACH_LOCAL, REACH_ORG, REACH_BANK) + +# Tag prefixes written by Cerebral.retain_episode. +TAG_ORG = "org:" +TAG_REPO = "repo:" +TAG_PROJECT_SCOPE = "project_scope:" +TAG_TASK = "task:" + + +def org_of(repo_full_name: str) -> str: + """Return the owner (first path segment) of ``owner/repo``.""" + return (repo_full_name or "").strip("/").split("/", 1)[0] + + +def repo_tags(repo_full_name: str) -> list[str]: + """Tags identifying a repository: ``[org:, repo:]``.""" + return [f"{TAG_ORG}{org_of(repo_full_name)}", f"{TAG_REPO}{repo_full_name}"] + + +def scope_tags(repo_full_name: str, scope_topic_id: str) -> list[str]: + """Tags identifying one project scope: ``[org:, repo:, project_scope:]``.""" + return [*repo_tags(repo_full_name), f"{TAG_PROJECT_SCOPE}{scope_topic_id}"] + + +def mental_model_id(tags: Iterable[str]) -> str: + """Deterministic mental-model id for a tag scope. + + ``mm-`` plus the first 32 hex chars of ``sha1("|".join(sorted(tags)))``. + """ + digest = hashlib.sha1("|".join(sorted(tags)).encode("utf-8")).hexdigest() + return f"mm-{digest[:32]}" + + +def mental_model_scopes( + repo_full_name: str, + scope_topic_ids: Sequence[str] | None, + include_repo: bool, +) -> list[list[str]]: + """Tag scopes whose briefings an agent reads: its scopes first, then the repo.""" + scopes = [scope_tags(repo_full_name, sid) for sid in (scope_topic_ids or [])] + if include_repo: + scopes.append(repo_tags(repo_full_name)) + return scopes + + +def mental_model_ids( + repo_full_name: str, + scope_topic_ids: Sequence[str] | None, + include_repo: bool, +) -> list[str]: + """Mental-model ids an agent reads, in briefing order (scopes, then repo).""" + return [ + mental_model_id(t) + for t in mental_model_scopes(repo_full_name, scope_topic_ids, include_repo) + ] + + +def clamp_reach(requested: str | None, configured: str) -> str: + """Clamp a requested reach to the configured one. Unknown values mean ``local``.""" + req = (requested or REACH_LOCAL).strip().lower() + if req not in REACHES: + req = REACH_LOCAL + cfg = configured if configured in REACHES else REACH_LOCAL + return REACHES[min(REACHES.index(req), REACHES.index(cfg))] + + +def local_tags( + repo_full_name: str, + scope_topic_ids: Sequence[str] | None, + include_repo: bool, +) -> list[str]: + """Tags of the local pool (OR-ed): ``project_scope:`` ids, plus ``repo:`` if requested.""" + tags = [f"{TAG_PROJECT_SCOPE}{sid}" for sid in (scope_topic_ids or [])] + if include_repo: + tags.insert(0, f"{TAG_REPO}{repo_full_name}") + return tags + + +def build_local_groups( + repo_full_name: str, + scope_topic_ids: Sequence[str] | None, + include_repo: bool, +) -> list[Any]: + """Hindsight tag groups for the local pool. + + One ``any_strict`` leaf: a fact matches when it carries any of the local + tags (and untagged facts never match). Returns ``[]`` when there is no + local tag at all — callers must then skip the local pool rather than + search the whole bank. + """ + tags = local_tags(repo_full_name, scope_topic_ids, include_repo) + if not tags: + return [] + from hindsight_api.engine.search.tags import TagGroupLeaf + + return [TagGroupLeaf(tags=tags, match="any_strict")] + + +def build_remote_groups(repo_full_name: str, reach: str) -> list[Any] | None: + """Hindsight tag groups for the remote pool, or ``None`` for ``local`` reach. + + - ``org``: ``org:`` AND NOT ``repo:`` + - ``bank``: NOT ``repo:`` + """ + if reach not in (REACH_ORG, REACH_BANK): + return None + from hindsight_api.engine.search.tags import TagGroupLeaf, TagGroupNot + + not_own = TagGroupNot( + filter=TagGroupLeaf(tags=[f"{TAG_REPO}{repo_full_name}"], match="any_strict") + ) + if reach == REACH_ORG: + return [ + TagGroupLeaf(tags=[f"{TAG_ORG}{org_of(repo_full_name)}"], match="any_strict"), + not_own, + ] + return [not_own] + + +def build_reflect_groups( + repo_full_name: str, + scope_topic_ids: Sequence[str] | None, + include_repo: bool, + reach: str, +) -> list[Any] | None: + """Tag groups for a tool-side reflect over ``reach``. + + - ``local``: the local pool + - ``org``: the local pool OR ``org:`` + - ``bank``: no filter (the whole bank) + """ + if reach == REACH_BANK: + return None + tags = local_tags(repo_full_name, scope_topic_ids, include_repo) + if reach == REACH_ORG: + tags = [*tags, f"{TAG_ORG}{org_of(repo_full_name)}"] + if not tags: + return [] + from hindsight_api.engine.search.tags import TagGroupLeaf + + return [TagGroupLeaf(tags=tags, match="any_strict")] + + +def tag_value(tags: Iterable[str] | None, prefix: str) -> str | None: + """Return the value of the first tag with ``prefix`` (e.g. ``task:``), or None.""" + for tag in tags or (): + if tag.startswith(prefix): + return tag[len(prefix) :] + return None + + +def is_own_repo_fact(tags: Iterable[str] | None, repo_full_name: str) -> bool: + """True when a fact belongs to ``repo_full_name``. + + Covers old data that has no ``repo:`` tag but a ``project_scope:`` id under + the repo, which ``NOT repo:`` cannot exclude. + """ + own_scope = f"{TAG_PROJECT_SCOPE}{repo_full_name}" + for tag in tags or (): + if tag == f"{TAG_REPO}{repo_full_name}": + return True + if tag == own_scope or tag.startswith(own_scope + "/"): + return True + return False diff --git a/src/codespy/agents/memory/recall.py b/src/codespy/agents/memory/recall.py new file mode 100644 index 0000000..2e24362 --- /dev/null +++ b/src/codespy/agents/memory/recall.py @@ -0,0 +1,137 @@ +"""Recall monitoring: record what Prefrontal gave to the agent. + +This module has no dependencies on hippocampus, prefrontal or cerebral, +so all of them can import it without circular import risks. +""" + +from __future__ import annotations + +import contextvars +import threading +import uuid +from collections.abc import Iterator, Set +from contextlib import contextmanager +from dataclasses import dataclass, field +from datetime import UTC, datetime +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +class RecallRecord(BaseModel): + """One Prefrontal recall (pre-call load or recall_memory tool call). + + Attributes: + id: UUID identifying this recall record. + run_id: Run identifier this recall belongs to. + task: Consumer task name (e.g. "code_review", "review" for run-level load). + kind: "load" for pre-call aload, "tool" for recall_memory. + timestamp: UTC time the recall started. + query: The query used (context facet for load, truncated query for tool). + reach: Effective reach (local/org/bank; clamped for tool calls). + reflects: The reflects config at call time. + status: Outcome classification. + text: Exact string returned to the agent ("" on load error). + model: LLM model(s) that answered, comma-joined; "" = no LLM used. + llm_calls: Number of LLM calls (reflect iterations + rewrite). + input_tokens: LLM prompt tokens + embedding tokens. + output_tokens: LLM completion tokens. + input_cost: Cost in USD for input tokens. + output_cost: Cost in USD for output tokens. + latency_ms: Wall time from start to result, in milliseconds. + details: JSON-serializable extra data per kind. + """ + + id: uuid.UUID = Field(default_factory=uuid.uuid4) + run_id: str = Field(default="") + task: str = Field(default="") + kind: Literal["load", "tool"] + timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC)) + query: str = Field(default="") + reach: str = Field(default="local") + reflects: int = Field(default=0) + status: Literal["ok", "empty", "limit", "error"] = Field(default="ok") + text: str = Field(default="") + model: str = Field(default="") + llm_calls: int = Field(default=0) + input_tokens: int = Field(default=0) + output_tokens: int = Field(default=0) + input_cost: float = Field(default=0.0) + output_cost: float = Field(default=0.0) + latency_ms: int = Field(default=0) + details: dict[str, Any] = Field(default_factory=dict) + + +@dataclass +class RecallUsage: + """Thread-safe accumulator for one recall's usage. + + Used via `current_recall_usage` contextvar so that concurrent recalls + (e.g., per-scope in asyncio.gather) don't mix their metrics. + """ + + input_tokens: int = 0 + output_tokens: int = 0 + input_cost: float = 0.0 + output_cost: float = 0.0 + llm_calls: int = 0 + _models: set[str] = field(default_factory=set) + _lock: threading.Lock = field(default_factory=threading.Lock) + + def add_llm( + self, + model: str, + input_tokens: int, + output_tokens: int, + input_cost: float, + output_cost: float, + ) -> None: + """Record one LLM call.""" + with self._lock: + self.input_tokens += input_tokens + self.output_tokens += output_tokens + self.input_cost += input_cost + self.output_cost += output_cost + self.llm_calls += 1 + if model: + self._models.add(model) + + def add_embedding(self, tokens: int, cost: float) -> None: + """Record one embedding call (counts as input only).""" + with self._lock: + self.input_tokens += tokens + self.input_cost += cost + + @property + def models(self) -> Set[str]: + """Return the set of models used.""" + with self._lock: + return set(self._models) + + def to_model_field(self) -> str: + """Comma-joined models, empty if none.""" + with self._lock: + return ",".join(sorted(self._models)) + + +# Context variable to attribute LLM/embedding calls to the current recall. +current_recall_usage: contextvars.ContextVar[RecallUsage | None] = contextvars.ContextVar( + "current_recall_usage", default=None +) + + +@contextmanager +def track_recall_usage() -> Iterator[RecallUsage]: + """Set current_recall_usage for the duration of the context manager. + + Usage: + with track_recall_usage() as usage: + ... # LLM calls here will add to usage + # usage now holds the accumulated metrics + """ + usage = RecallUsage() + token = current_recall_usage.set(usage) + try: + yield usage + finally: + current_recall_usage.reset(token) diff --git a/src/codespy/agents/review/audit/agent.py b/src/codespy/agents/review/audit/agent.py index 96cc8f8..90f99f2 100644 --- a/src/codespy/agents/review/audit/agent.py +++ b/src/codespy/agents/review/audit/agent.py @@ -7,13 +7,14 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.context_memory import Topic from codespy.agents.memory.hippocampus.episode import submit_episode_save +from codespy.agents.memory.prefrontal import with_prefrontal_memory from codespy.agents.review.models import Issue, ReviewContext from codespy.agents.review.helpers import deepest_common_folder from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal if TYPE_CHECKING: from codespy.agents.review.scope.models import ScopeResult @@ -59,6 +60,7 @@ def _call_auditor( run_id: str | None, scopes: list["ScopeResult"] | None, topics: list["ScopeResult"] | None = None, + prefrontal_memory: str | None = None, ) -> tuple[dspy.Prediction, Hippocampus | None, "EpisodeStore | None", dict[str, str] | None]: """Execute the auditor predictor (with or without Hippocampus memory). @@ -66,6 +68,10 @@ def _call_auditor( Tuple of (result, hippo, store, artifacts) where hippo/store/artifacts are None when memory is disabled. """ + # Prefrontal context (None when inactive); never given to Hippocampus. + pf_kwargs: dict[str, str] = ( + {"prefrontal_memory": prefrontal_memory} if prefrontal_memory is not None else {} + ) question = ( f"final audit of {review_context.pr_context.repo_slug}: " f"pull request {review_context.pr_context.pr_number} " @@ -76,7 +82,8 @@ def _call_auditor( initial_memory: ContextMemory | None = None topic_ids: list[str] | None = None store = None - if self._settings.get_memory_enabled("audit") and scopes: + hippo: Hippocampus | None = None + if self._settings.memory.enabled and scopes: store = get_episode_store(self._settings) if store is not None: # Build topic_ids from scope topics @@ -92,30 +99,28 @@ def _call_auditor( 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(scope_topic) - - inject_context_memory(auditor) - hippo = Hippocampus( - task_name="audit", - budget=self._settings.get_memory_budget("audit"), - question=question, - run_id=run_id, - initial_memory=initial_memory, - topics=scope_topics if scope_topics else None, - ) - result = auditor( - context_memory=hippo.context_memory, - pr_title=review_context.pr_context.pr_title, - summary=review_context.pr_context.summary, - all_issues=all_issues, - ) + # 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(scope_topic) + + hippo = Hippocampus( + task_name="audit", + budget=self._settings.get_memory_budget(), + question=question, + run_id=run_id, + initial_memory=initial_memory, + topics=scope_topics if scope_topics else None, + ) + result = auditor( + pr_title=review_context.pr_context.pr_title, + summary=review_context.pr_context.summary, + all_issues=all_issues, + **pf_kwargs, + ) + if hippo is not None: hippo.observe(result) _artifacts = { "audit": ( @@ -125,11 +130,6 @@ def _call_auditor( } return result, hippo, store, _artifacts else: - result = auditor( - pr_title=review_context.pr_context.pr_title, - summary=review_context.pr_context.summary, - all_issues=all_issues, - ) return result, None, None, None def forward( @@ -159,9 +159,20 @@ def forward( "NEEDS_DISCUSSION" if all_issues else "APPROVE", ) + repo_full_name = review_context.pr_context.repo_full_name + # Use shared run-level recall (already loaded by pipeline) + prefrontal_memory = review_context.prefrontal_memory or None + # Get Prefrontal for tool calls only (prefrontal_memory is already loaded by pipeline) + pf = get_prefrontal( + self._settings, + "audit", + repo_full_name, + scope_topic_ids=None, + ) + sig = with_prefrontal_memory(AuditSignature) if prefrontal_memory else AuditSignature auditor = ContextSafe( - dspy.ChainOfThought(AuditSignature), - AuditSignature, + dspy.ChainOfThought(sig), + sig, name="audit", max_iters=self._settings.get_max_iters("audit"), max_llm_calls=self._settings.get_max_llm_calls("audit"), @@ -177,15 +188,27 @@ def forward( run_id, scopes, topics, + prefrontal_memory=prefrontal_memory, ) # Background episode save (after SignatureContext closes) if hippo is not None and store is not None: + cerebral = get_cerebral(self._settings) + _cerebral = cerebral def _persist(): try: - hippo.end_episode(store, artifacts=_artifacts) + hippo.end_episode( + store, + artifacts=_artifacts, + recalls=pf.recalls if pf else None, + ) except Exception: logger.warning("Audit episode save failed", exc_info=True) + if _cerebral is not None and hippo.episode is not None: + try: + _cerebral.retain_episode(hippo.episode, repo_full_name=repo_full_name) + except Exception: + logger.warning("Background cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="audit-episode-save") return result.quality_assessment, result.recommendation diff --git a/src/codespy/agents/review/code_review/agent.py b/src/codespy/agents/review/code_review/agent.py index f672f85..4a2a88d 100644 --- a/src/codespy/agents/review/code_review/agent.py +++ b/src/codespy/agents/review/code_review/agent.py @@ -11,8 +11,9 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save +from codespy.agents.memory.prefrontal import with_prefrontal_memory from codespy.agents.review.models import Issue, IssueCategory, ReviewContext from codespy.agents.review.scope.models import ScopeResult from codespy.agents.review.helpers import ( @@ -22,7 +23,7 @@ restore_repo_paths, ) from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -271,15 +272,25 @@ async def _review_scope( scope_root = resolve_scope_root(repo_path, scope.subroot) tools, contexts = await self._create_tools(scope_root) try: + repo_full_name = review_context.pr_context.repo_full_name + # Compute scope topic ID once, reused for Prefrontal and Hippocampus load + scope_topic_id = scope.topic(repo_full_name).id + # Get Prefrontal for tool calls only (prefrontal_memory is already loaded by pipeline) + pf = get_prefrontal( + self._settings, "code_review", repo_full_name, scope_topic_ids=[scope_topic_id] + ) + sig = with_prefrontal_memory(CodeReviewSignature) if review_context.prefrontal_memory else CodeReviewSignature + recall_tool = pf.recall_tool() if pf else None + agent_tools = [*tools, recall_tool] if recall_tool else tools agent = ContextSafe( dspy.RLM( - CodeReviewSignature, - tools=tools, + sig, + tools=agent_tools, max_iters=max_iters, max_llm_calls=self._settings.get_max_llm_calls("code_review"), ), - CodeReviewSignature, - tools=tools, + sig, + tools=agent_tools, name="code_review", max_iters=max_iters, max_llm_calls=self._settings.get_max_llm_calls("code_review"), @@ -291,22 +302,24 @@ async def _review_scope( ) hippo: Hippocampus | None = None async with SignatureContext("code_review", self._cost_tracker): + # Prefrontal: use shared run-level recall (never given to Hippocampus) + pf_kwargs: dict[str, Any] = {} + if review_context.prefrontal_memory: + pf_kwargs["prefrontal_memory"] = review_context.prefrontal_memory # 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"): + if self._settings.memory.enabled: 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: + if store is not None: + scope_initial_memory = store.load_context( + task="code_review", + topic_ids=[scope_topic_id], + ) + 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) question = ( f"review code change of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -314,45 +327,43 @@ async def _review_scope( f"{review_context.pr_context.summary}" ) pr_ctx = review_context.pr_context - topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] - inject_context_memory(agent) + topics = [scope.topic(pr_ctx.repo_full_name), pr_ctx.to_topic()] if pr else [] hippo = Hippocampus( task_name="code_review", - budget=self._settings.get_memory_budget("code_review"), + budget=self._settings.get_memory_budget(), question=question, run_id=run_id, initial_memory=scope_initial_memory, topics=topics, ) - result = await agent.acall( - context_memory=hippo.context_memory, - scope=scoped, - categories=categories, - ) + result = await agent.acall( + scope=scoped, + categories=categories, + **pf_kwargs, + ) + issues = [ + issue + for issue in (result.issues or []) + if issue.confidence >= self._settings.review.min_confidence + ] + if hippo is not None: await hippo.aobserve(result) - issues = [ - issue - for issue in (result.issues or []) - if issue.confidence >= self._settings.min_confidence - ] # Fire-and-forget background episode save _artifacts = {"review": issues_to_markdown(issues)} - def _persist(h=hippo, s=store, a=_artifacts): + cerebral = get_cerebral(self._settings) + def _persist( + h=hippo, s=store, a=_artifacts, c=cerebral, r=repo_full_name, p=pf + ): try: - h.end_episode(s, artifacts=a) + h.end_episode(s, artifacts=a, recalls=p.recalls if p else None) except Exception: logger.warning("Background code_review episode save failed", exc_info=True) + if c is not None and h.episode is not None: + try: + c.retain_episode(h.episode, repo_full_name=r) + except Exception: + logger.warning("Background cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="code-review-episode-save") - else: - result = await agent.acall( - scope=scoped, - categories=categories, - ) - issues = [ - issue - for issue in (result.issues or []) - if issue.confidence >= self._settings.min_confidence - ] restore_repo_paths(issues, scope.subroot) logger.debug(f" Scope {scope.subroot}: {len(issues)} code review issues") return issues diff --git a/src/codespy/agents/review/doc/agent.py b/src/codespy/agents/review/doc/agent.py index 039f2b4..db00803 100644 --- a/src/codespy/agents/review/doc/agent.py +++ b/src/codespy/agents/review/doc/agent.py @@ -11,8 +11,9 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save +from codespy.agents.memory.prefrontal import with_prefrontal_memory from codespy.agents.review.models import Issue, IssueCategory, ReviewContext from codespy.agents.review.scope.models import ScopeResult from codespy.agents.review.doc.doc_extractor import extract_documentation @@ -24,7 +25,7 @@ restore_repo_paths, ) from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal logger = logging.getLogger(__name__) @@ -200,9 +201,15 @@ async def _review_scope( logger.debug(f" No patches in {scope.subroot}, skipping doc review") return [] try: + repo_full_name = review_context.pr_context.repo_full_name + # Get Prefrontal for tool calls only (prefrontal_memory is already loaded by pipeline) + pf = get_prefrontal( + self._settings, "doc", repo_full_name, scope_topic_ids=None + ) + sig = with_prefrontal_memory(DocReviewSignature) if review_context.prefrontal_memory else DocReviewSignature reviewer = ContextSafe( - dspy.ChainOfThought(DocReviewSignature), - DocReviewSignature, + dspy.ChainOfThought(sig), + sig, name="doc", max_iters=max_iters, max_llm_calls=self._settings.get_max_llm_calls("doc"), @@ -213,22 +220,25 @@ async def _review_scope( ) hippo: Hippocampus | None = None async with SignatureContext("doc", self._cost_tracker): + # Prefrontal: use shared run-level recall (never given to Hippocampus) + pf_kwargs: dict[str, Any] = {} + if review_context.prefrontal_memory: + pf_kwargs["prefrontal_memory"] = review_context.prefrontal_memory # Load own prior "doc" episode for this scope scope_initial_memory: ContextMemory | None = None store = None - if self._settings.get_memory_enabled("doc"): + if self._settings.memory.enabled: 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: + 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) question = ( f"review documentation of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -237,47 +247,43 @@ async def _review_scope( ) pr_ctx = review_context.pr_context topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] - inject_context_memory(reviewer) hippo = Hippocampus( task_name="doc", - budget=self._settings.get_memory_budget("doc"), + budget=self._settings.get_memory_budget(), question=question, run_id=run_id, initial_memory=scope_initial_memory, topics=topics, ) - result = await reviewer.aforward( - context_memory=hippo.context_memory, - patches=patches, - documentation=documentation, - categories=[IssueCategory.DOCUMENTATION], - ) + result = await reviewer.acall( + patches=patches, + documentation=documentation, + categories=[IssueCategory.DOCUMENTATION], + **pf_kwargs, + ) + issues = [ + issue + for issue in (result.issues or []) + if issue.confidence >= self._settings.review.min_confidence + ] + if hippo is not None: await hippo.aobserve(result) - issues = [ - issue - for issue in (result.issues or []) - if issue.confidence >= self._settings.min_confidence - ] # Fire-and-forget background episode save _artifacts = {"review": issues_to_markdown(issues)} - def _persist(h=hippo, s=store, a=_artifacts): + cerebral = get_cerebral(self._settings) + def _persist( + h=hippo, s=store, a=_artifacts, c=cerebral, r=repo_full_name, p=pf + ): try: - h.end_episode(s, artifacts=a) + h.end_episode(s, artifacts=a, recalls=p.recalls if p else None) except Exception: logger.warning("Background doc episode save failed", exc_info=True) + if c is not None and h.episode is not None: + try: + c.retain_episode(h.episode, repo_full_name=r) + except Exception: + logger.warning("Background cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="doc-episode-save") - else: - result = await asyncio.to_thread( - reviewer, - patches=patches, - documentation=documentation, - categories=[IssueCategory.DOCUMENTATION], - ) - issues = [ - issue - for issue in (result.issues or []) - if issue.confidence >= self._settings.min_confidence - ] restore_repo_paths(issues, scope.subroot) logger.debug(f" Scope {scope.subroot}: {len(issues)} doc issues") return issues diff --git a/src/codespy/agents/review/doc/doc_extractor.py b/src/codespy/agents/review/doc/doc_extractor.py index 1e63ed9..db07d3f 100644 --- a/src/codespy/agents/review/doc/doc_extractor.py +++ b/src/codespy/agents/review/doc/doc_extractor.py @@ -6,6 +6,9 @@ from codespy.tools.storage import EntryType, FileSystem, TreeNode +# CHANGELOG heading pattern for trimming (3rd `## ` heading marks cutoff) +_CHANGELOG_HEADING_RE = re.compile(r"^## ", re.MULTILINE) + logger = logging.getLogger(__name__) # Filename patterns recognised as documentation (case-insensitive). @@ -50,10 +53,35 @@ def _collect_doc_paths(node: TreeNode, prefix: str = "") -> list[str]: return paths +def _trim_changelog(content: str) -> str: + """Trim CHANGELOG content to keep only preamble + 2 newest sections. + + Keeps text before the 3rd line matching ``^## `` (preamble + 2 newest + sections, e.g. ``[Unreleased]`` + latest release). If fewer than 3 such + headings, keeps the file unchanged. Appends ``\n[older entries omitted]`` + when trimmed. + + Args: + content: The full CHANGELOG content. + + Returns: + Trimmed content if 3+ headings found, else original content. + """ + matches = list(_CHANGELOG_HEADING_RE.finditer(content)) + if len(matches) < 3: + return content + + # Keep everything up to the 3rd heading (exclusive) + cutoff_pos = matches[2].start() + trimmed = content[:cutoff_pos].rstrip() + return trimmed + "\n[older entries omitted]" + + def extract_documentation(scope_root: Path) -> str: """Extract documentation content from a scope directory. Single tree scan at depth 2, fast-fail if no doc files found. + CHANGELOG files are trimmed to preamble + 2 newest sections. Returns: Concatenated documentation with ``=== filename ===`` headers, @@ -89,10 +117,15 @@ def extract_documentation(scope_root: Path) -> str: logger.debug(f"Skipping unreadable doc file {path}: {content.error}") continue - if not content.content.strip(): + text = content.content + if not text.strip(): logger.debug(f"Skipping empty doc file {path}") continue - parts.append(f"=== {path} ===\n{content.content}") + # Trim CHANGELOG files to reduce token volume + if path.lower().startswith("changelog"): + text = _trim_changelog(text) + + parts.append(f"=== {path} ===\n{text}") return "\n\n".join(parts) diff --git a/src/codespy/agents/review/helpers.py b/src/codespy/agents/review/helpers.py index 646acae..3dffd09 100644 --- a/src/codespy/agents/review/helpers.py +++ b/src/codespy/agents/review/helpers.py @@ -6,7 +6,7 @@ import os from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from codespy.agents.review.models import Issue from codespy.tools.git.models import ChangedFile @@ -259,3 +259,11 @@ def issues_to_markdown(issues: list[Issue]) -> str: lines.append("---") lines.append("") return "\n".join(lines) + + +def scope_package_names(scope: ScopeResult | Any) -> list[str]: + """Package identity of a scope, when its manifest declares one (Prefrontal queries).""" + manifest = getattr(scope, "package_manifest", None) + if manifest is not None and getattr(manifest, "package_name", None): + return [manifest.package_name] + return [] diff --git a/src/codespy/agents/review/models.py b/src/codespy/agents/review/models.py index b8e7b08..8845c53 100644 --- a/src/codespy/agents/review/models.py +++ b/src/codespy/agents/review/models.py @@ -91,16 +91,21 @@ class ReviewContext(BaseModel): """Evolving pipeline state threaded through review stages. Carries the immutable PR identity and runtime pipeline metadata. - Context memory is loaded independently by each module from its own prior episodes. + The prefrontal_memory field holds the shared run-level recall text + that is loaded once and passed to all consumer agents. """ pr_context: PRContext = Field( description="Immutable PR identity (repo, number, title, summary)" ) memory: ContextMemory | None = Field( - default=None, description="Unused — each module loads its own prior episodes. Kept for API compatibility." + default=None, description="Deprecated — use prefrontal_memory instead. Kept for API compatibility." ) metadata: ReviewMetadata | None = Field(default=None, description="Runtime pipeline state") + prefrontal_memory: str = Field( + default="", + description="Shared run-level Prefrontal recall text, loaded once and passed to all consumer agents", + ) class Issue(BaseModel): diff --git a/src/codespy/agents/review/scope/__init__.py b/src/codespy/agents/review/scope/__init__.py index 04cab8e..b599070 100644 --- a/src/codespy/agents/review/scope/__init__.py +++ b/src/codespy/agents/review/scope/__init__.py @@ -1,2 +1,15 @@ -from codespy.agents.review.scope.agent import MANIFEST_FILES, MANIFEST_GLOBS, ScopeResolver, derive_sparse_paths +from codespy.agents.review.scope.agent import MANIFEST_FILES, MANIFEST_GLOBS, ScopeResolver, derive_sparse_paths, build_sparse_patterns, AI_DIRS, AI_FILES from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType + +__all__ = [ + "MANIFEST_FILES", + "MANIFEST_GLOBS", + "AI_DIRS", + "AI_FILES", + "ScopeResolver", + "derive_sparse_paths", + "build_sparse_patterns", + "PackageManifest", + "ScopeResult", + "ScopeType", +] diff --git a/src/codespy/agents/review/scope/agent.py b/src/codespy/agents/review/scope/agent.py index 93939ab..1e1a85a 100644 --- a/src/codespy/agents/review/scope/agent.py +++ b/src/codespy/agents/review/scope/agent.py @@ -19,13 +19,14 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save +from codespy.agents.memory.prefrontal import build_facets, with_prefrontal_memory from codespy.agents.review.models import ReviewContext from codespy.agents.review.scope.manifest_parser import extract_package_name from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal 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 @@ -373,27 +374,55 @@ class ScopeRefinementSignature(dspy.Signature): ) -def derive_sparse_paths(changed_files: list[str]) -> list[str]: - """Derive minimal sparse checkout paths from changed files. +# AI instruction directories and files (used for sparse patterns) +AI_DIRS: list[str] = [".claude/", ".kilo/", ".agent/", ".ai/", ".cursor/", ".codex/"] +AI_FILES: list[str] = ["AGENTS.md", "CLAUDE.md", "SKILL.md"] + + +def build_sparse_patterns( + changed_files: list[str], + include_scope_roots: bool = True, + include_manifests: bool = True, + include_ai_files: bool = True, +) -> list[str]: + """Build anchored sparse checkout patterns from changed files. + + In non-cone sparse-checkout mode (gitignore-style semantics): + - Patterns with a leading/middle slash are anchored to repo root + - Patterns without a slash match at any depth (expensive in treeless clones) + - Patterns ending with slash match directories at any depth + + This function emits anchored patterns only: // + to limit materialization to ancestor dirs of changed files. Args: - changed_files: List of changed file paths + changed_files: List of changed file paths (relative to repo root) + include_scope_roots: Include scope subtree roots (e.g., /packages/auth/) + include_manifests: Include anchored manifest patterns + include_ai_files: Include anchored AI instruction dir/file patterns Returns: - List of sparse paths for git sparse-checkout + List of anchored sparse patterns for git sparse-checkout """ - scope_roots: set[str] = set() + patterns: set[str] = set() + ancestor_dirs: set[str] = set() + # Build scope roots from changed files (for subtree checkout) + scope_roots: set[str] = set() for filepath in changed_files: parts = filepath.split("/") if len(parts) <= 1: - continue # Root-level file, handled by "/*" below + continue # Root-level file + + # Collect all ancestor directories of this file + for depth in range(1, len(parts)): + ancestor_dirs.add("/".join(parts[:depth])) # Strategy A: Find scope indicator and take the next directory found_indicator = False for i, part in enumerate(parts[:-1]): # Skip filename if part.lower() in SCOPE_INDICATOR_DIRS and i + 1 < len(parts) - 1: - scope_root = "/".join(parts[: i + 2]) + "/" + scope_root = "/".join(parts[: i + 2]) scope_roots.add(scope_root) found_indicator = True break @@ -401,36 +430,65 @@ def derive_sparse_paths(changed_files: list[str]) -> list[str]: # Strategy B: No indicator found -- use depth-2 prefix if not found_indicator: depth = min(2, len(parts) - 1) - scope_roots.add("/".join(parts[:depth]) + "/") - - # Always include root-level files for root manifests - paths = sorted(scope_roots) - paths.append("/*") - - # Explicitly add root manifest files to ensure they are checked out - # in sparse/treeless clones (/* pattern doesn't always work reliably) - for manifest in MANIFEST_FILES: - paths.append(manifest) - for manifest_pattern in MANIFEST_GLOBS: - # For glob patterns like *.csproj, we need to add the pattern itself - paths.append(manifest_pattern) - - # Agent config directories — project instructions for ReAct agents - paths.extend( - [ - ".claude/", - ".kilo/", - ".agent/", - ".ai/", - ".cursor/", - ".codex/", - "AGENTS.md", - "CLAUDE.md", - "SKILL.md", - ] - ) + scope_roots.add("/".join(parts[:depth])) + + # Always include root in ancestor dirs + ancestor_dirs.add(".") - return paths + # Scope roots (for subtree checkout) - anchored as // + if include_scope_roots: + for scope_root in scope_roots: + patterns.add(f"/{scope_root}/") + + # Root-level files + patterns.add("/*") + + # Manifest files - anchored to each ancestor dir to avoid repo-wide matching + if include_manifests: + for ancestor in ancestor_dirs: + for manifest in MANIFEST_FILES: + if ancestor == ".": + patterns.add(f"/{manifest}") + else: + patterns.add(f"/{ancestor}/{manifest}") + for ancestor in ancestor_dirs: + for manifest_pattern in MANIFEST_GLOBS: + if ancestor == ".": + patterns.add(f"/{manifest_pattern}") + else: + patterns.add(f"/{ancestor}/{manifest_pattern}") + + # AI instruction directories and files - anchored to each ancestor dir + if include_ai_files: + for ancestor in ancestor_dirs: + for ai_dir in AI_DIRS: + if ancestor == ".": + patterns.add(f"/{ai_dir}") + else: + patterns.add(f"/{ancestor}/{ai_dir}") + for ancestor in ancestor_dirs: + for ai_file in AI_FILES: + if ancestor == ".": + patterns.add(f"/{ai_file}") + else: + patterns.add(f"/{ancestor}/{ai_file}") + + return sorted(patterns) + + +def derive_sparse_paths(changed_files: list[str]) -> list[str]: + """Derive minimal sparse checkout paths from changed files. + + Deprecated: Use build_sparse_patterns() for new code. + This function is kept for backward compatibility. + + Args: + changed_files: List of changed file paths + + Returns: + List of sparse paths for git sparse-checkout + """ + return build_sparse_patterns(changed_files) class ScopeResolver(dspy.Module): @@ -441,6 +499,8 @@ def __init__(self) -> None: super().__init__() self._cost_tracker = get_cost_tracker() self._settings = get_settings() + self.prefrontal_memory: str = "" + self.prefrontal_sections: list[tuple[str, str]] = [] async def _create_tools(self, repo_path: Path) -> tuple[list[Any], list[Any]]: """Create tools for the scope agent: filesystem + ripgrep. @@ -502,8 +562,6 @@ async def _ensure_repo(self, pr: PullRequest, repo_path: Path, is_local: bool) - repo.git.update_environment(GIT_TERMINAL_PROMPT="0") repo.git.fetch("origin", pr.head_sha, "--depth", "1") repo.git.checkout(pr.head_sha) - # Ensure manifests at root + parent dirs - await self._ensure_manifests(repo_path, changed_file_paths) return changed_file_paths = [f.filename for f in pr.changed_files] @@ -530,60 +588,6 @@ async def _ensure_repo(self, pr: PullRequest, repo_path: Path, is_local: bool) - ) logger.info("Clone complete: %s", repo_path) - # Ensure manifest files at root and parent directories are checked out - await self._ensure_manifests(repo_path, changed_file_paths) - - async def _ensure_manifests(self, repo_path: Path, changed_files: list[str]) -> None: - """Ensure manifest files at root and parent directories are checked out. - - Sparse/treeless clones may not materialize manifests at ancestor directories. - This explicitly checks out known manifest files at: - - Repository root - - Every ancestor directory of every changed file path - - Args: - repo_path: Path to the repository root - changed_files: List of changed file paths - """ - from git import Repo - - # Collect all ancestor directories of changed files - parent_dirs: set[str] = set() - for filepath in changed_files: - parts = filepath.split("/") - for depth in range(1, len(parts)): # skip filename, collect dirs - parent_dirs.add("/".join(parts[:depth])) - - # Build list of manifest paths to check - manifest_paths: list[str] = [] - - # Root manifests - for manifest in MANIFEST_FILES: - manifest_paths.append(manifest) - - # Parent manifests - for parent in parent_dirs: - for manifest in MANIFEST_FILES: - manifest_paths.append(f"{parent}/{manifest}") - - # Checkout missing manifests - try: - from git import Repo - from git.exc import GitCommandError - - repo = Repo(repo_path) - for path in manifest_paths: - if not (repo_path / path).exists(): - try: - repo.git.checkout("HEAD", "--", path) - logger.debug("Checked out manifest: %s", path) - except GitCommandError: - pass # File doesn't exist in repo — expected - except Exception as e: - logger.warning("Unexpected error checking out manifest %s: %s", path, e) - except Exception as e: - logger.warning("Failed to ensure manifests: %s", e) - def _resolve( self, repo_path: Path, changed_files: list[ChangedFile], repo: str ) -> tuple[list[ScopeResult], list[ChangedFile]]: @@ -597,7 +601,7 @@ def _resolve( Returns: Tuple of (active scopes, orphan files) """ - excluded_dirs = self._settings.excluded_directories + excluded_dirs = self._settings.review.excluded_directories manifests = self._discover_manifests(repo_path, changed_files, excluded_dirs) logger.info( "Manifest discovery at %s found %d manifest(s): %s", @@ -1050,24 +1054,50 @@ async def _refine_scopes( max_iters = self._settings.get_max_iters("scope") tools, contexts = await self._create_tools(repo_path) try: + pf = get_prefrontal(self._settings, "scope", pr.repo_full_name, include_repo=True) + sig = ( + with_prefrontal_memory(ScopeRefinementSignature) if pf else ScopeRefinementSignature + ) + recall_tool = pf.recall_tool() if pf else None + agent_tools = [*tools, recall_tool] if recall_tool else tools agent = ContextSafe( dspy.RLM( - ScopeRefinementSignature, - tools=tools, + sig, + tools=agent_tools, max_iters=max_iters, max_llm_calls=self._settings.get_max_llm_calls("scope"), ), - ScopeRefinementSignature, - tools=tools, + sig, + tools=agent_tools, name="scope", max_iters=max_iters, max_llm_calls=self._settings.get_max_llm_calls("scope"), rlm_threshold=self._settings.get_rlm_threshold("react"), ) hippo: Hippocampus | None = None + store = None async with SignatureContext("scope", self._cost_tracker): - if self._settings.get_memory_enabled("scope"): + # Prefrontal: prior knowledge from Cerebral (never given to Hippocampus) + pf_kwargs: dict[str, Any] = {} + if pf is not None: + scope_pf_text = await pf.aload( + build_facets( + "scope", + pr.repo_full_name, + pr_title=pr.title or "", + paths=[f.filename for s in scopes for f in s.changed_files] + + [f.filename for f in orphans], + summary=pr.body or "", + ) + ) + pf_kwargs["prefrontal_memory"] = scope_pf_text + self.prefrontal_memory = scope_pf_text + self.prefrontal_sections = pf.last_sections + if self._settings.memory.enabled: + store = get_episode_store(self._settings) + scope_initial_memory: ContextMemory | None = None + if store is not None: question = ( f"refine scopes of {review_context.pr_context.repo_slug}: " f"PR #{review_context.pr_context.pr_number} " @@ -1075,45 +1105,34 @@ async def _refine_scopes( f"{review_context.pr_context.summary}" ) # Scope resolver loads its own prior episodes (no memory inheritance) - 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) - inject_context_memory(agent) + # 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) hippo = Hippocampus( task_name="scope", - budget=self._settings.get_memory_budget("scope"), + budget=self._settings.get_memory_budget(), question=question, run_id=run_id, initial_memory=scope_initial_memory, ) - result = await agent.acall( - context_memory=hippo.context_memory, - candidates=candidates_str, - orphan_files=[f.filename for f in orphans], - pr_title=pr.title or "No title", - pr_description=pr.body or "No description", - project_instructions=project_instructions, - ) + result = await agent.acall( + candidates=candidates_str, + orphan_files=[f.filename for f in orphans], + pr_title=pr.title or "No title", + pr_description=pr.body or "No description", + project_instructions=project_instructions, + **pf_kwargs, + ) + if hippo is not None: await hippo.aobserve(result) - else: - result = await agent.acall( - candidates=candidates_str, - orphan_files=[f.filename for f in orphans], - pr_title=pr.title or "No title", - pr_description=pr.body or "No description", - project_instructions=project_instructions, - ) # Collect all changed files (from scopes + orphans) all_files = [f for s in scopes for f in s.changed_files] + orphans @@ -1164,16 +1183,28 @@ async def _refine_scopes( hippo.bind_topics(scope_topics, stamp_topic_ids) # Fire-and-forget background episode save + cerebral = get_cerebral(self._settings) if hippo 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 ) + _cerebral = cerebral + _repo_full_name = pr.repo_full_name def _persist(): try: - hippo.end_episode(store, artifacts={"scopes": scope_desc}) + hippo.end_episode( + store, + artifacts={"scopes": scope_desc}, + recalls=pf.recalls if pf else None, + ) except Exception: logger.warning("Background scope episode save failed", exc_info=True) + if _cerebral is not None and hippo.episode is not None: + try: + _cerebral.retain_episode(hippo.episode, repo_full_name=_repo_full_name) + except Exception: + logger.warning("Background cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="scope-episode-save") return final_scopes @@ -1193,13 +1224,17 @@ async def aforward( Returns: List of ScopeResult """ + # Reset prefrontal memory at start of each run + self.prefrontal_memory = "" + self.prefrontal_sections = [] + # Local bindings from review_context metadata pr = review_context.metadata.pr repo_path = review_context.metadata.repo_path is_local = review_context.metadata.is_local run_id = review_context.metadata.run_id - excluded_dirs = self._settings.excluded_directories + excluded_dirs = self._settings.review.excluded_directories reviewable_files = [f for f in pr.changed_files if should_review_file(f, excluded_dirs)] if not reviewable_files: return [] diff --git a/src/codespy/agents/review/summary/agent.py b/src/codespy/agents/review/summary/agent.py index 0318645..550acba 100644 --- a/src/codespy/agents/review/summary/agent.py +++ b/src/codespy/agents/review/summary/agent.py @@ -7,12 +7,13 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +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.memory.prefrontal import with_prefrontal_memory from codespy.agents.review.helpers import deepest_common_folder from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal if TYPE_CHECKING: from codespy.agents.review.models import PRContext @@ -55,6 +56,7 @@ def forward( run_id: str | None = None, scopes: list["ScopeResult"] | None = None, topics: list[Topic] | None = None, + prefrontal_memory: str = "", ) -> str: """Generate a PR summary. @@ -65,6 +67,7 @@ def forward( run_id: Pipeline run identifier scopes: List of resolved scopes for per-scope episode persistence topics: Optional list of Topic objects for auto-tagging + prefrontal_memory: Shared run-level Prefrontal recall text (already loaded by pipeline) Returns: Summary string @@ -78,7 +81,7 @@ def forward( initial_memory: ContextMemory | None = None store = None topic_ids: list[str] | None = None - if self._settings.get_memory_enabled("summary") and scopes: + if self._settings.memory.enabled and scopes: store = get_episode_store(self._settings) if store is not None: # Build topic_ids from scope topics @@ -95,9 +98,19 @@ def forward( else: logger.info("No prior summary episode found") + repo_full_name = pr_context.repo_full_name + # Get Prefrontal for tool calls only (prefrontal_memory is already loaded) + pf = get_prefrontal( + self._settings, + "summary", + repo_full_name, + scope_topic_ids=None, + include_repo=False, + ) + sig = with_prefrontal_memory(PRSummarySignature) if prefrontal_memory else PRSummarySignature summarizer = ContextSafe( - dspy.ChainOfThought(PRSummarySignature), - PRSummarySignature, + dspy.ChainOfThought(sig), + sig, name="summary", max_iters=self._settings.get_max_iters("summary"), max_llm_calls=self._settings.get_max_llm_calls("summary"), @@ -109,7 +122,11 @@ def forward( hippo: Hippocampus | None = None with SignatureContext("summary", self._cost_tracker): - if self._settings.get_memory_enabled("summary") and store is not None: + # Prefrontal: use shared run-level recall (never given to Hippocampus) + pf_kwargs: dict[str, str] = {} + if prefrontal_memory: + pf_kwargs["prefrontal_memory"] = prefrontal_memory + if store is not None: # Build topics list for Hippocampus scope_topics: list[Topic] = [] for scope in scopes or []: @@ -117,38 +134,44 @@ def forward( if scope_topic: scope_topics.append(scope_topic) - inject_context_memory(summarizer) hippo = Hippocampus( task_name="summary", - budget=self._settings.get_memory_budget("summary"), + budget=self._settings.get_memory_budget(), question=question, run_id=run_id, initial_memory=initial_memory, topics=scope_topics if scope_topics else topics, ) - result = summarizer( - context_memory=hippo.context_memory, - pr_title=pr_context.pr_title, - pr_description=pr_context.pr_description, - changed_file_paths=changed_file_paths, - patches=patches, - ) + result = summarizer( + pr_title=pr_context.pr_title, + pr_description=pr_context.pr_description, + changed_file_paths=changed_file_paths, + patches=patches, + **pf_kwargs, + ) + if hippo is not None: hippo.observe(result) # Fire-and-forget episode save _summary_text = result.summary + cerebral = get_cerebral(self._settings) + _cerebral = cerebral + # Get recalls from Prefrontal for tool calls only + _recalls = pf.recalls if pf else None def _persist(): try: - hippo.end_episode(store, artifacts={"summary": _summary_text}) + hippo.end_episode( + store, + artifacts={"summary": _summary_text}, + recalls=_recalls, + ) except Exception: logger.warning("Background summary episode save failed", exc_info=True) + if _cerebral is not None and hippo.episode is not None: + try: + _cerebral.retain_episode(hippo.episode, repo_full_name=repo_full_name) + except Exception: + logger.warning("Background summary cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="summary-episode-save") - else: - result = summarizer( - pr_title=pr_context.pr_title, - pr_description=pr_context.pr_description, - changed_file_paths=changed_file_paths, - patches=patches, - ) logger.info(f"PR summary: {result.summary[:80]}...") return result.summary diff --git a/src/codespy/agents/review/supply_chain/agent.py b/src/codespy/agents/review/supply_chain/agent.py index 2f14fb1..265efc8 100644 --- a/src/codespy/agents/review/supply_chain/agent.py +++ b/src/codespy/agents/review/supply_chain/agent.py @@ -11,8 +11,9 @@ from codespy.agents import SignatureContext, get_cost_tracker from codespy.agents.context_safe import ContextSafe -from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus, inject_context_memory +from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus from codespy.agents.memory.hippocampus.episode import submit_episode_save +from codespy.agents.memory.prefrontal import with_prefrontal_memory from codespy.agents.review.models import Issue, IssueCategory, ReviewContext from codespy.agents.review.scope.models import ScopeResult from codespy.agents.review.helpers import ( @@ -22,7 +23,7 @@ strip_prefix, ) from codespy.config import get_settings -from codespy.config_memory import get_episode_store +from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server logger = logging.getLogger(__name__) @@ -369,14 +370,29 @@ async def _review_scope( try: # Combine scoped filesystem tools with shared OSV tools all_tools = scoped_tools + osv_tools + repo_full_name = review_context.pr_context.repo_full_name + # Compute scope topic ID once, reused for Prefrontal and Hippocampus load + scope_topic_id = scope.topic(repo_full_name).id + # Get Prefrontal for tool calls only (prefrontal_memory is already loaded by pipeline) + pf = get_prefrontal( + self._settings, "supply_chain", repo_full_name, scope_topic_ids=[scope_topic_id] + ) + sig = ( + with_prefrontal_memory(SupplyChainSecuritySignature) + if review_context.prefrontal_memory + else SupplyChainSecuritySignature + ) + recall_tool = pf.recall_tool() if pf else None + if recall_tool: + all_tools = [*all_tools, recall_tool] supply_chain_agent = ContextSafe( dspy.RLM( - SupplyChainSecuritySignature, + sig, tools=all_tools, max_iters=supply_chain_max_iters, max_llm_calls=self._settings.get_max_llm_calls("supply_chain"), ), - SupplyChainSecuritySignature, + sig, tools=all_tools, name="supply_chain", max_iters=supply_chain_max_iters, @@ -391,22 +407,24 @@ async def _review_scope( # Track supply_chain signature costs separately hippo: Hippocampus | None = None async with SignatureContext("supply_chain", self._cost_tracker): + # Prefrontal: use shared run-level recall (never given to Hippocampus) + pf_kwargs: dict[str, Any] = {} + if review_context.prefrontal_memory: + pf_kwargs["prefrontal_memory"] = review_context.prefrontal_memory # 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"): + if self._settings.memory.enabled: 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: + if store is not None: + scope_initial_memory = store.load_context( + task="supply_chain", + topic_ids=[scope_topic_id], + ) + 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) question = ( f"review supply chain of {scope.repo}: {scope.subroot}: " f"pull request {review_context.pr_context.pr_number} " @@ -414,49 +432,45 @@ async def _review_scope( f"{review_context.pr_context.summary}" ) pr_ctx = review_context.pr_context - topics = [scope.topic(pr.repo_full_name), pr_ctx.to_topic()] if pr else [] - inject_context_memory(supply_chain_agent) + topics = [scope.topic(pr_ctx.repo_full_name), pr_ctx.to_topic()] if pr else [] hippo = Hippocampus( task_name="supply_chain", - budget=self._settings.get_memory_budget("supply_chain"), + budget=self._settings.get_memory_budget(), question=question, run_id=run_id, initial_memory=scope_initial_memory, topics=topics, ) - result = await supply_chain_agent.acall( - context_memory=hippo.context_memory, - manifest_path=manifest_path, - lock_file_path=lock_file_path, - package_manager=package_manager, - category=IssueCategory.SECURITY, - ) + result = await supply_chain_agent.acall( + manifest_path=manifest_path, + lock_file_path=lock_file_path, + package_manager=package_manager, + category=IssueCategory.SECURITY, + **pf_kwargs, + ) + issues = [ + issue + for issue in result.issues + if issue.confidence >= self._settings.review.min_confidence + ] + if hippo is not None: await hippo.aobserve(result) - issues = [ - issue - for issue in result.issues - if issue.confidence >= self._settings.min_confidence - ] # Fire-and-forget background episode save _artifacts = {"review": issues_to_markdown(issues)} - def _persist(h=hippo, s=store, a=_artifacts): + cerebral = get_cerebral(self._settings) + def _persist( + h=hippo, s=store, a=_artifacts, c=cerebral, r=repo_full_name, p=pf + ): try: - h.end_episode(s, artifacts=a) + h.end_episode(s, artifacts=a, recalls=p.recalls if p else None) except Exception: logger.warning("Background supply_chain episode save failed", exc_info=True) + if c is not None and h.episode is not None: + try: + c.retain_episode(h.episode, repo_full_name=r) + except Exception: + logger.warning("Background cerebral retain failed", exc_info=True) submit_episode_save(_persist, name="supply-chain-episode-save") - else: - result = await supply_chain_agent.acall( - manifest_path=manifest_path, - lock_file_path=lock_file_path, - package_manager=package_manager, - category=IssueCategory.SECURITY, - ) - issues = [ - issue - for issue in result.issues - if issue.confidence >= self._settings.min_confidence - ] # Restore repo-root-relative paths in reported issues restore_repo_paths(issues, scope.subroot) logger.debug( diff --git a/src/codespy/cli.py b/src/codespy/cli.py index 99cdc90..d8e6cff 100644 --- a/src/codespy/cli.py +++ b/src/codespy/cli.py @@ -76,12 +76,12 @@ def config( console.print(Panel("[bold]Current Configuration[/bold]", title="codespy")) # Show non-sensitive settings - console.print(f"[bold]Model:[/bold] {settings.default_model}") - console.print(f"[bold]AWS Region:[/bold] {settings.aws_region}") - console.print(f"[bold]Reasoning Effort:[/bold] {settings.default_reasoning_effort}") + console.print(f"[bold]Model:[/bold] {settings.llm.default_model}") + console.print(f"[bold]AWS Region:[/bold] {settings.llm.aws_region}") + console.print(f"[bold]Reasoning Effort:[/bold] {settings.llm.default_reasoning_effort}") - console.print(f"[bold]Output Format:[/bold] {settings.output_format}") - console.print(f"[bold]Cache Directory:[/bold] {settings.cache_dir}") + console.print(f"[bold]Output Format:[/bold] {settings.review.output_format}") + console.print(f"[bold]Cache Directory:[/bold] {settings.review.cache_dir}") # Show GitHub token status github_token_source = get_github_token_source() @@ -105,9 +105,9 @@ def config( console.print(f"[bold]GitLab URL:[/bold] {settings.gitlab_url}") - openai_status = "[green]configured[/green]" if secret_value(settings.openai_api_key) else "[dim]not set[/dim]" + openai_status = "[green]configured[/green]" if secret_value(settings.llm.openai_api_key) else "[dim]not set[/dim]" anthropic_status = ( - "[green]configured[/green]" if secret_value(settings.anthropic_api_key) else "[dim]not set[/dim]" + "[green]configured[/green]" if secret_value(settings.llm.anthropic_api_key) else "[dim]not set[/dim]" ) console.print(f"[bold]OpenAI API Key:[/bold] {openai_status}") console.print(f"[bold]Anthropic API Key:[/bold] {anthropic_status}") diff --git a/src/codespy/cli_local.py b/src/codespy/cli_local.py index 60cdd43..bb62af4 100644 --- a/src/codespy/cli_local.py +++ b/src/codespy/cli_local.py @@ -81,9 +81,9 @@ def review_local( raise typer.Exit(1) from None if model: - settings.default_model = model + settings.llm.default_model = model if output: - settings.output_format = output # type: ignore + settings.review.output_format = output # type: ignore repo = Path(repo_path if repo_path else os.getcwd()).resolve() @@ -100,8 +100,8 @@ def review_local( f"[bold blue]Reviewing local changes[/bold blue]\n" f"[bold]Repository:[/bold] {repo}\n" f"[bold]Base ref:[/bold] {base_ref}\n" - f"[bold]Model:[/bold] {settings.default_model}\n" - f"[bold]Output:[/bold] {settings.output_format}", + f"[bold]Model:[/bold] {settings.llm.default_model}\n" + f"[bold]Output:[/bold] {settings.review.output_format}", title="codespy review-local", ) ) @@ -109,6 +109,7 @@ def review_local( try: from codespy.workflows.review.models import LocalReviewConfig from codespy.workflows.review.pipeline import ReviewPipeline + from codespy.workflows.review.reporters import StdoutReporter pipeline = ReviewPipeline(settings) @@ -116,22 +117,27 @@ def review_local( config = LocalReviewConfig(repo_path=repo, base_ref=base_ref, uncommitted=False) # Run review (model access always verified in pipeline) - result = pipeline(config) + # Episode saves are deferred; finish_memory runs them + result = pipeline.finish_memory(pipeline(config)) if result.llm_calls > 0: cost_str = f"${result.total_cost:.4f}" if result.total_cost > 0 else "N/A" + # Calculate input/output totals from signature_stats + total_in_tokens = sum(s.input_tokens for s in result.signature_stats) + total_out_tokens = sum(s.output_tokens for s in result.signature_stats) + total_in_cost = sum(s.input_cost for s in result.signature_stats) + total_out_cost = sum(s.output_cost for s in result.signature_stats) console.print( Panel( f"[bold]LLM Calls:[/bold] {result.llm_calls}\n" - f"[bold]Total Tokens:[/bold] {result.total_tokens:,}\n" + f"[bold]In Tokens:[/bold] {total_in_tokens:,} [bold]Out Tokens:[/bold] {total_out_tokens:,}\n" + f"[bold]In Cost:[/bold] ${total_in_cost:.4f} [bold]Out Cost:[/bold] ${total_out_cost:.4f}\n" f"[bold]Total Cost:[/bold] {cost_str}", title="Cost Summary", ) ) - from codespy.workflows.review.reporters import StdoutReporter - - stdout_reporter = StdoutReporter(format=settings.output_format, console=console) + stdout_reporter = StdoutReporter(format=settings.review.output_format, console=console) stdout_reporter.report(result) except Exception as e: @@ -195,9 +201,9 @@ def review_uncommitted( raise typer.Exit(1) from None if model: - settings.default_model = model + settings.llm.default_model = model if output: - settings.output_format = output # type: ignore + settings.review.output_format = output # type: ignore repo = Path(repo_path if repo_path else os.getcwd()).resolve() @@ -213,8 +219,8 @@ def review_uncommitted( Panel( f"[bold blue]Reviewing uncommitted changes[/bold blue]\n" f"[bold]Repository:[/bold] {repo}\n" - f"[bold]Model:[/bold] {settings.default_model}\n" - f"[bold]Output:[/bold] {settings.output_format}", + f"[bold]Model:[/bold] {settings.llm.default_model}\n" + f"[bold]Output:[/bold] {settings.review.output_format}", title="codespy review-uncommitted", ) ) @@ -222,6 +228,7 @@ def review_uncommitted( try: from codespy.workflows.review.models import LocalReviewConfig from codespy.workflows.review.pipeline import ReviewPipeline + from codespy.workflows.review.reporters import StdoutReporter pipeline = ReviewPipeline(settings) @@ -229,22 +236,27 @@ def review_uncommitted( config = LocalReviewConfig(repo_path=repo, uncommitted=True) # Run review (model access always verified in pipeline) - result = pipeline(config) + # Episode saves are deferred; finish_memory runs them + result = pipeline.finish_memory(pipeline(config)) if result.llm_calls > 0: cost_str = f"${result.total_cost:.4f}" if result.total_cost > 0 else "N/A" + # Calculate input/output totals from signature_stats + total_in_tokens = sum(s.input_tokens for s in result.signature_stats) + total_out_tokens = sum(s.output_tokens for s in result.signature_stats) + total_in_cost = sum(s.input_cost for s in result.signature_stats) + total_out_cost = sum(s.output_cost for s in result.signature_stats) console.print( Panel( f"[bold]LLM Calls:[/bold] {result.llm_calls}\n" - f"[bold]Total Tokens:[/bold] {result.total_tokens:,}\n" + f"[bold]In Tokens:[/bold] {total_in_tokens:,} [bold]Out Tokens:[/bold] {total_out_tokens:,}\n" + f"[bold]In Cost:[/bold] ${total_in_cost:.4f} [bold]Out Cost:[/bold] ${total_out_cost:.4f}\n" f"[bold]Total Cost:[/bold] {cost_str}", title="Cost Summary", ) ) - from codespy.workflows.review.reporters import StdoutReporter - - stdout_reporter = StdoutReporter(format=settings.output_format, console=console) + stdout_reporter = StdoutReporter(format=settings.review.output_format, console=console) stdout_reporter.report(result) except Exception as e: diff --git a/src/codespy/cli_remote.py b/src/codespy/cli_remote.py index 2761358..f8b13d8 100644 --- a/src/codespy/cli_remote.py +++ b/src/codespy/cli_remote.py @@ -95,13 +95,13 @@ def review( # Override settings if provided via CLI (CLI > env > yaml > defaults) if model: - settings.default_model = model + settings.llm.default_model = model if output: - settings.output_format = output # type: ignore + settings.review.output_format = output # type: ignore if stdout is not None: - settings.output_stdout = stdout + settings.review.output_stdout = stdout if git_comment is not None: - settings.output_git = git_comment + settings.review.output_git = git_comment # Validate URL format if not is_supported_url(pr_url): @@ -140,9 +140,9 @@ def review( # Build output destinations display output_destinations = [] - if settings.output_stdout: - output_destinations.append(f"stdout ({settings.output_format})") - if settings.output_git: + if settings.review.output_stdout: + output_destinations.append(f"stdout ({settings.review.output_format})") + if settings.review.output_git: output_destinations.append(f"{platform.title()} comment") output_display = ", ".join(output_destinations) if output_destinations else "[red]none[/red]" @@ -150,7 +150,7 @@ def review( Panel( f"[bold blue]Reviewing PR:[/bold blue] {pr_url}\n" f"[bold]Platform:[/bold] {platform.title()}\n" - f"[bold]Model:[/bold] {settings.default_model}\n" + f"[bold]Model:[/bold] {settings.llm.default_model}\n" f"[bold]Output:[/bold] {output_display}\n" f"[bold]{platform.title()} Token:[/bold] [green]found[/green] " f"[dim]({token_source})[/dim]", @@ -161,40 +161,58 @@ def review( try: from codespy.workflows.review.models import RemoteReviewConfig from codespy.workflows.review.pipeline import ReviewPipeline + from codespy.workflows.review.reporters import GitReporter, StdoutReporter pipeline = ReviewPipeline(settings) # Create remote review config config = RemoteReviewConfig(url=pr_url) - # Run review (model access always verified in pipeline) + # Run review phase (model access always verified in pipeline) + # Episode saves are deferred until finish_memory is called result = pipeline(config) - # Show cost summary + handle = None + try: + # Publish to git platform after audit (before memory phase) + if settings.review.output_git: + console.print(f"[dim]Posting review to {platform.title()}...[/dim]") + git_reporter = GitReporter(url=pr_url, settings=settings) + handle = git_reporter.publish(result) + console.print(f"[green]✓[/green] {platform.title()} review posted successfully") + finally: + # Run memory phase (saves, retain, consolidation) + # This runs even if git publication failed + result = pipeline.finish_memory(result) + + # Update the git review with full costs after memory phase + if handle and settings.review.output_git: + git_reporter = GitReporter(url=pr_url, settings=settings) + git_reporter.update(handle, result) + + # Show cost summary with full costs (after memory phase) if result.llm_calls > 0: cost_str = f"${result.total_cost:.4f}" if result.total_cost > 0 else "N/A" + # Calculate input/output totals from signature_stats + total_in_tokens = sum(s.input_tokens for s in result.signature_stats) + total_out_tokens = sum(s.output_tokens for s in result.signature_stats) + total_in_cost = sum(s.input_cost for s in result.signature_stats) + total_out_cost = sum(s.output_cost for s in result.signature_stats) console.print( Panel( f"[bold]LLM Calls:[/bold] {result.llm_calls}\n" - f"[bold]Total Tokens:[/bold] {result.total_tokens:,}\n" + f"[bold]In Tokens:[/bold] {total_in_tokens:,} [bold]Out Tokens:[/bold] {total_out_tokens:,}\n" + f"[bold]In Cost:[/bold] ${total_in_cost:.4f} [bold]Out Cost:[/bold] ${total_out_cost:.4f}\n" f"[bold]Total Cost:[/bold] {cost_str}", title="Cost Summary", ) ) - # Output results using reporters - from codespy.workflows.review.reporters import GitReporter, StdoutReporter - - if settings.output_stdout: - stdout_reporter = StdoutReporter(format=settings.output_format, console=console) + # Output results to stdout once at the end (after memory phase) + if settings.review.output_stdout: + stdout_reporter = StdoutReporter(format=settings.review.output_format, console=console) stdout_reporter.report(result) - if settings.output_git: - console.print(f"[dim]Posting review to {platform.title()}...[/dim]") - git_reporter = GitReporter(url=pr_url, settings=settings) - git_reporter.report(result) - console.print(f"[green]✓[/green] {platform.title()} review posted successfully") - except ValueError as e: console.print(f"[red]Error:[/red] {e}") raise typer.Exit(1) from None diff --git a/src/codespy/config.py b/src/codespy/config.py index a9d2d91..b3fd213 100644 --- a/src/codespy/config.py +++ b/src/codespy/config.py @@ -8,14 +8,7 @@ from pydantic import Field, SecretStr, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict -from codespy.config_utils import secret_value -from codespy.config_dspy import ( - ReasoningEffort, - RLMFallbackConfig, - SignatureConfig, - apply_rlm_fallback_env_overrides, - apply_signature_env_overrides, -) +from codespy.config_dspy import ReviewConfig, RLMFallbackConfig, SignatureConfig from codespy.config_git import ( GitHubConfig, GitLabConfig, @@ -26,26 +19,26 @@ set_github_token_source, set_gitlab_token_source, ) -from codespy.config_io import DEFAULT_EXCLUDED_DIRECTORIES, OutputFormat -from codespy.config_llm import ( - LLMConfig, - discover_anthropic_api_key, - discover_aws_credentials, - discover_gemini_api_key, - discover_openai_api_key, -) +from codespy.config_llm import LLMConfig from codespy.config_memory import ( + MEMORY_CONSOLIDATION, + MEMORY_MENTAL_MODELS, + MEMORY_RECALL, + MEMORY_RETAIN, REFLECTION_MODULES, LLMSettings, MemoryConfig, - apply_memory_env_overrides, + ReflectionModuleConfig, + reflection_module_config, reset_episode_store, ) +from codespy.config_utils import ( + apply_env_overrides, + build_env_map, + secret_value, +) if TYPE_CHECKING: - # Imported lazily inside get_memory_budget(): importing this at module level - # pulls in codespy.agents, whose __init__ imports dspy_config, which imports - # this module — a circular import that breaks every entrypoint. from codespy.agents.memory.hippocampus.budget import MemoryBudget @@ -66,10 +59,32 @@ "GitLabConfig", "SignatureConfig", "MemoryConfig", - "OutputFormat", ] +# Build the env map once at module level +# collapsed_paths drops the prefix from descendant env var names. +# full_name_paths keeps the full name for specific leaves (exceptions). +_ENV_MAP = build_env_map( + sections={ + "llm": LLMConfig, + "review": ReviewConfig, + "memory": MemoryConfig, + }, + collapsed_paths={ + ("llm",), + ("memory", "hippocampus"), + ("memory", "cerebral"), + ("memory", "prefrontal"), + }, + full_name_paths={ + ("llm", "retries"), + ("llm", "timeout"), + ("memory", "prefrontal", "reflects"), + }, +) + + def _load_yaml_config() -> dict[str, Any]: """Load YAML config file if it exists. @@ -115,52 +130,7 @@ class Settings(BaseSettings): github: GitHubConfig = Field(default_factory=GitHubConfig) gitlab: GitLabConfig = Field(default_factory=GitLabConfig) memory: MemoryConfig = Field(default_factory=MemoryConfig) - rlm_fallback: RLMFallbackConfig = Field(default_factory=RLMFallbackConfig) - - # Flat signature configs (signature_name -> SignatureConfig) - signatures: dict[str, SignatureConfig] = Field(default_factory=dict) - - # Top-level defaults (also available via env vars DEFAULT_MODEL, etc.) - default_model: str = "anthropic/claude-opus-4-6" - extraction_model: str | None = None # TwoStepAdapter extraction (falls back to default_model) - default_max_iters: int = 5 - default_max_llm_calls: int = 8 - # Provider reasoning budget; LiteLLM maps this to each provider's native parameter. - default_reasoning_effort: ReasoningEffort = "medium" - default_temperature: float = 0.2 - # Output token budget per completion. Must be set explicitly: when it is - # omitted LiteLLM silently falls back to its own 4096 default, which - # truncates reasoning models (thinking tokens are charged against this - # budget) and long structured outputs. 32000 matches the output ceiling of - # the Claude 4.x tier and satisfies dspy.LM's >=16000 guard for OpenAI - # reasoning models; new_lm() clamps it down to each model's real ceiling. - default_max_tokens: int = 32000 - - # Global LLM reliability settings - llm_retries: int = 2 # Number of retries for LLM API calls - llm_timeout: int = 240 # Timeout in seconds for LLM calls - - # 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) - - # Top-level settings - output_format: OutputFormat = "markdown" - cache_dir: Path = Path.home() / ".cache" / "codespy" - - # Output destinations - output_stdout: bool = True # Enable stdout output (markdown or json) - output_git: bool = True # Enable Git platform review comments (GitHub PR or GitLab MR) - - # File exclusion settings - excluded_directories: list[str] = Field(default=DEFAULT_EXCLUDED_DIRECTORIES) + review: ReviewConfig = Field(default_factory=ReviewConfig) # GitHub token (can also use GITHUB_TOKEN or GH_TOKEN env var) github_token: SecretStr | None = None @@ -172,25 +142,12 @@ class Settings(BaseSettings): gitlab_url: str = "https://gitlab.com" # GITLAB_URL for self-hosted instances gitlab_auto_discover_token: bool = True # GITLAB_AUTO_DISCOVER_TOKEN - # LLM provider settings (flat for simple env var access) - aws_region: str = "us-east-1" - aws_access_key_id: SecretStr | None = None - aws_secret_access_key: SecretStr | None = None - aws_profile: str | None = None - openai_api_key: SecretStr | None = None - anthropic_api_key: SecretStr | None = None - gemini_api_key: SecretStr | None = None - - # Auto-discovery toggles (flat for env var access) - auto_discover_aws: bool = True # AUTO_DISCOVER_AWS - auto_discover_openai: bool = True # AUTO_DISCOVER_OPENAI - auto_discover_anthropic: bool = True # AUTO_DISCOVER_ANTHROPIC - auto_discover_gemini: bool = True # AUTO_DISCOVER_GEMINI - - # Helper methods for signature config def get_signature_config(self, signature_name: str) -> SignatureConfig: - """Get config for a signature.""" - return self.signatures.get(signature_name, SignatureConfig()) + """Get config for a signature. + + Only returns actual signature configs, not other review fields. + """ + return getattr(self.review, signature_name, SignatureConfig()) def is_signature_enabled(self, signature_name: str) -> bool: """Check if a signature is enabled.""" @@ -199,52 +156,96 @@ def is_signature_enabled(self, signature_name: str) -> bool: def get_max_iters(self, name: str) -> int: """Get max_iters for a signature or reflection module (name-specific or default).""" if name in REFLECTION_MODULES: - config = getattr(self.memory, name) - return config.max_iters or self.default_max_iters + config = reflection_module_config(self.memory.hippocampus, name) + return config.max_iters or self.llm.default_max_iters config = self.get_signature_config(name) - return config.max_iters or self.default_max_iters + return config.max_iters or self.llm.default_max_iters def get_max_llm_calls(self, name: str) -> int: """Get max_llm_calls for a signature or reflection module (name-specific or default).""" if name in REFLECTION_MODULES: - config = getattr(self.memory, name) - return config.max_llm_calls or self.default_max_llm_calls + config = reflection_module_config(self.memory.hippocampus, name) + return config.max_llm_calls or self.llm.default_max_llm_calls config = self.get_signature_config(name) - return config.max_llm_calls or self.default_max_llm_calls + return config.max_llm_calls or self.llm.default_max_llm_calls def get_llm_config(self, name: str) -> LLMSettings: """Resolve the LLM settings for one named unit of LLM work. ``name`` addresses either a signature or a memory reflection module:: - "code_review" -> signatures.code_review - "distiller" -> memory.distiller + "code_review" -> review.code_review + "memory_distiller" -> memory.hippocampus.distiller + "memory_retain" -> memory.cerebral.retain + "memory_consolidation" -> memory.cerebral.consolidation + "memory_mental_models" -> memory.cerebral.mental_models - Every field falls back to its top-level ``default_*`` counterpart, so + Every field falls back to its ``llm.default_*`` counterpart, so the result has no ``None`` fields and callers never re-apply fallbacks. Args: name: A signature name, or a reflection module name - (see ``REFLECTION_MODULES``). + (see ``REFLECTION_MODULES``), or "memory_retain", + "memory_consolidation", "memory_mental_models". Returns: The fully resolved settings for ``name``. """ - if name in REFLECTION_MODULES: - config = getattr(self.memory, name) + if name == MEMORY_RETAIN: + # Cerebral uses only a model from retain config; all other fields use llm defaults + model = self.memory.cerebral.retain.model or self.llm.default_model + config = ReflectionModuleConfig() # Empty config - all fields fall back to defaults + defaults = self.llm + elif name == MEMORY_CONSOLIDATION: + # Consolidation model: falls back to retain model, then default + model = ( + self.memory.cerebral.consolidation.model + or self.memory.cerebral.retain.model + or self.llm.default_model + ) + config = ReflectionModuleConfig() # Empty config - all fields fall back to defaults + defaults = self.llm + elif name == MEMORY_RECALL: + # Recall model: falls back to retain model, then default + model = ( + self.memory.prefrontal.recall.model + or self.memory.cerebral.retain.model + or self.llm.default_model + ) + config = ReflectionModuleConfig() # Empty config - all fields fall back to defaults + defaults = self.llm + elif name == MEMORY_MENTAL_MODELS: + # Mental-model refresh model: falls back to prefrontal.recall → retain → default + model = ( + self.memory.cerebral.mental_models.model + or self.memory.prefrontal.recall.model + or self.memory.cerebral.retain.model + or self.llm.default_model + ) + config = ReflectionModuleConfig() # Empty config - all fields fall back to defaults + defaults = self.llm + elif name in REFLECTION_MODULES: + config = reflection_module_config(self.memory.hippocampus, name) + model = config.model or self.llm.default_model + defaults = self.llm + elif name == "default": + # Pure defaults for configure_dspy + model = self.llm.default_model + config = ReflectionModuleConfig() # Empty config + defaults = self.llm else: config = self.get_signature_config(name) + model = config.model or self.llm.default_model + defaults = self.llm - model = config.model or self.default_model - module_extraction = getattr(config, "extraction_model", None) return LLMSettings( model=model, - extraction_model=module_extraction or self.extraction_model or model, - reasoning_effort=config.reasoning_effort or self.default_reasoning_effort, + extraction_model=self.llm.extraction_model or model, + reasoning_effort=config.reasoning_effort or defaults.default_reasoning_effort, temperature=( - config.temperature if config.temperature is not None else self.default_temperature + config.temperature if config.temperature is not None else defaults.default_temperature ), - max_tokens=config.max_tokens or self.default_max_tokens, + max_tokens=config.max_tokens or defaults.default_max_tokens, ) def get_scan_unchanged(self, signature_name: str) -> bool: @@ -256,29 +257,19 @@ def get_scan_unchanged(self, signature_name: str) -> bool: config = self.get_signature_config(signature_name) return config.scan_unchanged if config.scan_unchanged is not None else False - # Helper methods for per-signature memory config (Hippocampus) - def get_memory_enabled(self, signature_name: str) -> bool: - """Whether Hippocampus memory is enabled for a signature. - - Per-signature ``memory.enabled`` overrides ``memory.default_enabled``. - """ - config = self.get_signature_config(signature_name).memory - return config.enabled if config.enabled is not None else self.memory.default_enabled - - def get_memory_budget(self, signature_name: str) -> "MemoryBudget": - """Resolve the ``MemoryBudget`` for a signature. + def get_memory_budget(self) -> "MemoryBudget": + """Resolve the ``MemoryBudget`` for memory operations. - Token budgets are global (``memory.default_*``); only ``enabled`` - supports per-signature overrides. + Token budgets are global (``memory.hippocampus.*``). """ from codespy.agents.memory.hippocampus.budget import MemoryBudget return MemoryBudget( - max_context_memory_tokens=self.memory.max_context_memory_tokens, - max_context_item_tokens=self.memory.max_context_item_tokens, - max_trajectory_tokens=self.memory.max_trajectory_tokens, - max_question_tokens=self.memory.max_question_tokens, - compact_trajectory=self.memory.compact_trajectory, + max_hippocampus_tokens=self.memory.hippocampus.max_hippocampus_tokens, + max_hippocampus_item_tokens=self.memory.hippocampus.max_hippocampus_item_tokens, + max_trajectory_tokens=self.memory.hippocampus.max_trajectory_tokens, + max_question_tokens=self.memory.hippocampus.max_question_tokens, + compact_trajectory=self.memory.hippocampus.compact_trajectory, ) def get_scope_skip_refinement(self) -> bool: @@ -302,21 +293,21 @@ def get_rlm_threshold(self, module_type: str) -> float: Returns: Threshold ratio (0.0-1.0), or 1.0 if disabled. """ - if not self.rlm_fallback.enabled: + if not self.llm.rlm_fallback.enabled: return 1.0 - return getattr(self.rlm_fallback, f"{module_type}_threshold", 1.0) + return getattr(self.llm.rlm_fallback, f"{module_type}_threshold", 1.0) def log_signature_configs(self) -> None: """Log all signature and reflection module LLM configurations.""" logger.info("RLM fallback configuration:") logger.info( - f" enabled={self.rlm_fallback.enabled}, " - f"react_threshold={self.rlm_fallback.react_threshold}, " - f"chain_of_thought_threshold={self.rlm_fallback.chain_of_thought_threshold}, " - f"predict_threshold={self.rlm_fallback.predict_threshold}" + f" enabled={self.llm.rlm_fallback.enabled}, " + f"react_threshold={self.llm.rlm_fallback.react_threshold}, " + f"chain_of_thought_threshold={self.llm.rlm_fallback.chain_of_thought_threshold}, " + f"predict_threshold={self.llm.rlm_fallback.predict_threshold}" ) logger.info("Signature configurations:") - for sig_name, sig_config in self.signatures.items(): + for sig_name, sig_config in self.review.signatures().items(): status = "enabled" if sig_config.enabled else "disabled" llm = self.get_llm_config(sig_name) logger.info( @@ -335,6 +326,19 @@ def log_signature_configs(self) -> None: f"reasoning_effort={llm.reasoning_effort}, temperature={llm.temperature}, " f"max_tokens={llm.max_tokens}" ) + # Prefrontal recall model (also refreshes briefings) + pf_llm = self.get_llm_config(MEMORY_RECALL) + logger.info( + f" {MEMORY_RECALL}: model={pf_llm.model}, " + f"reflects={self.memory.prefrontal.reflects}" + ) + # Consolidation model + cons_llm = self.get_llm_config(MEMORY_CONSOLIDATION) + logger.info(f" {MEMORY_CONSOLIDATION}: model={cons_llm.model}") + # Mental-models refresh model (only when reflects > 0) + if self.memory.prefrontal.reflects > 0: + mm_llm = self.get_llm_config(MEMORY_MENTAL_MODELS) + logger.info(f" {MEMORY_MENTAL_MODELS}: model={mm_llm.model}") @model_validator(mode="before") @classmethod @@ -344,12 +348,8 @@ def load_yaml_config(cls, values: dict[str, Any]) -> dict[str, Any]: Priority: Environment Variables > YAML Config > Defaults """ yaml_config = _load_yaml_config() - yaml_config = apply_signature_env_overrides(yaml_config) - # MEMORY_* env vars target the nested `memory` model, which - # pydantic-settings cannot populate on its own (no env_nested_delimiter). - yaml_config = apply_memory_env_overrides(yaml_config) - # RLM_FALLBACK_* env vars target the nested `rlm_fallback` model. - yaml_config = apply_rlm_fallback_env_overrides(yaml_config) + # Apply unified env overrides before merging + yaml_config = apply_env_overrides(yaml_config, _ENV_MAP) # Merge YAML config into values only if not already set (env vars take precedence) for key, val in yaml_config.items(): @@ -360,7 +360,11 @@ def load_yaml_config(cls, values: dict[str, Any]) -> dict[str, Any]: @model_validator(mode="after") def resolve_github_token(self) -> "Settings": - """Auto-discover GitHub token if not explicitly set.""" + """Auto-discover GitHub token if not explicitly set. + + Precedence: flat github_token (GITHUB_TOKEN env) → flat gh_token (GH_TOKEN env) + → YAML github.token → auto-discovery. + """ def is_placeholder(token: str) -> bool: """Check if token looks like a placeholder.""" @@ -368,35 +372,41 @@ def is_placeholder(token: str) -> bool: token_lower = token.lower() return any(p in token_lower for p in placeholders) - # First check nested config - nested_val = secret_value(self.github.token) - if nested_val and not is_placeholder(nested_val): - self.github_token = self.github.token # SecretStr → SecretStr - set_github_token_source("YAML config or GITHUB_TOKEN environment variable") - return self + def clear_placeholders(): + """Clear any placeholder values from both fields.""" + gh_val = secret_value(self.github_token) + if gh_val and is_placeholder(gh_val): + self.github_token = None + nested_val = secret_value(self.github.token) + if nested_val and is_placeholder(nested_val): + self.github.token = None + + # First, clear any placeholders + clear_placeholders() - # If github_token is set and not a placeholder, use it + # Precedence 1: flat github_token (GITHUB_TOKEN env / .env) gh_val = secret_value(self.github_token) - if gh_val and not is_placeholder(gh_val): + if gh_val: self.github.token = self.github_token # SecretStr → SecretStr set_github_token_source("GITHUB_TOKEN environment variable or .env file") return self - # If GH_TOKEN is set and not a placeholder, use it + # Precedence 2: flat gh_token (GH_TOKEN env) gh_token_val = secret_value(self.gh_token) - if gh_token_val and not is_placeholder(gh_token_val): + if gh_token_val: self.github_token = self.gh_token # SecretStr → SecretStr self.github.token = self.gh_token set_github_token_source("GH_TOKEN environment variable") return self - # Clear placeholder if present - gh_val2 = secret_value(self.github_token) - if gh_val2 and is_placeholder(gh_val2): - self.github_token = None - self.github.token = None + # Precedence 3: YAML github.token (already set if present) + nested_val = secret_value(self.github.token) + if nested_val: + self.github_token = self.github.token # SecretStr → SecretStr + set_github_token_source("YAML config (github.token)") + return self - # Try auto-discovery if enabled + # Precedence 4: auto-discovery auto_discover = self.github.auto_discover_token and self.github_auto_discover_token if auto_discover: @@ -416,7 +426,11 @@ def is_placeholder(token: str) -> bool: @model_validator(mode="after") def resolve_gitlab_token(self) -> "Settings": - """Auto-discover GitLab token if not explicitly set.""" + """Auto-discover GitLab token if not explicitly set. + + URL precedence: GITLAB_URL env → YAML gitlab.url → default (https://gitlab.com) + Token precedence: GITLAB_TOKEN env → YAML gitlab.token → auto-discovery + """ def is_placeholder(token: str) -> bool: """Check if token looks like a placeholder.""" @@ -424,31 +438,42 @@ def is_placeholder(token: str) -> bool: token_lower = token.lower() return any(p in token_lower for p in placeholders) - # First check nested config - nested_val = secret_value(self.gitlab.token) - if nested_val and not is_placeholder(nested_val): - self.gitlab_token = self.gitlab.token # SecretStr → SecretStr - set_gitlab_token_source("YAML config or GITLAB_TOKEN environment variable") - return self - - # Sync URL from nested config - if self.gitlab.url: + def clear_placeholders(): + """Clear any placeholder values from both fields.""" + token_val = secret_value(self.gitlab_token) + if token_val and is_placeholder(token_val): + self.gitlab_token = None + nested_val = secret_value(self.gitlab.token) + if nested_val and is_placeholder(nested_val): + self.gitlab.token = None + + # Clear placeholders first + clear_placeholders() + + # URL resolution: env wins over YAML + # Check if GITLAB_URL came from env (via model_fields_set) + if "gitlab_url" in self.model_fields_set: + # Env var was set, keep it and sync to nested + self.gitlab.url = self.gitlab_url + else: + # No env var, sync from YAML to flat (YAML wins over default) self.gitlab_url = self.gitlab.url - # If gitlab_token is set and not a placeholder, use it + # Precedence 1: flat gitlab_token (GITLAB_TOKEN env / .env) token_val = secret_value(self.gitlab_token) - if token_val and not is_placeholder(token_val): + if token_val: self.gitlab.token = self.gitlab_token # SecretStr → SecretStr set_gitlab_token_source("GITLAB_TOKEN environment variable or .env file") return self - # Clear placeholder if present - token_val2 = secret_value(self.gitlab_token) - if token_val2 and is_placeholder(token_val2): - self.gitlab_token = None - self.gitlab.token = None + # Precedence 2: YAML gitlab.token (already set if present) + nested_val = secret_value(self.gitlab.token) + if nested_val: + self.gitlab_token = self.gitlab.token # SecretStr → SecretStr + set_gitlab_token_source("YAML config (gitlab.token)") + return self - # Try auto-discovery if enabled + # Precedence 3: auto-discovery auto_discover = self.gitlab.auto_discover_token and self.gitlab_auto_discover_token if auto_discover: @@ -469,106 +494,9 @@ def is_placeholder(token: str) -> bool: @model_validator(mode="after") def expand_paths(self) -> "Settings": """Expand ~ in paths to the user's home directory.""" - self.cache_dir = Path(self.cache_dir).expanduser().resolve() + self.review.cache_dir = Path(self.review.cache_dir).expanduser().resolve() return self - @model_validator(mode="after") - def sync_llm_settings(self) -> "Settings": - """Sync LLM settings between nested and flat fields. - - Delegates to LLMConfig.sync_from_flat which enforces: - env vars (flat fields from pydantic-settings) > YAML/nested > defaults. - """ - merged = self.llm.sync_from_flat( - openai_api_key=self.openai_api_key, - anthropic_api_key=self.anthropic_api_key, - gemini_api_key=self.gemini_api_key, - aws_region=self.aws_region, - aws_access_key_id=self.aws_access_key_id, - aws_secret_access_key=self.aws_secret_access_key, - ) - # Propagate merged values back to flat fields - for field, value in merged.items(): - if value is not None: - setattr(self, field, value) - return self - - @model_validator(mode="after") - def resolve_llm_credentials(self) -> "Settings": - """Auto-discover LLM provider credentials if not explicitly set.""" - - def is_placeholder(value: str | None) -> bool: - """Check if value looks like a placeholder.""" - if not value: - return True - placeholders = ["xxx", "your", "key", "example", "placeholder", "null"] - value_lower = value.lower() - return any(p in value_lower for p in placeholders) - - # AWS credentials auto-discovery - if not secret_value(self.aws_access_key_id) or not secret_value(self.aws_secret_access_key): - auto_discover = self.llm.auto_discover_aws and self.auto_discover_aws - if auto_discover: - access_key, secret_key, region, profile, source = discover_aws_credentials() - if access_key and secret_key: - self.aws_access_key_id = SecretStr(access_key) - self.aws_secret_access_key = SecretStr(secret_key) - self.llm.aws_access_key_id = SecretStr(access_key) - self.llm.aws_secret_access_key = SecretStr(secret_key) - if region: - self.aws_region = region - self.llm.aws_region = region - if profile: - self.aws_profile = profile - self.llm.aws_profile = profile - logger.debug(f"AWS credentials discovered from: {source}") - else: - logger.debug("AWS credentials auto-discovery is disabled") - - # OpenAI API key auto-discovery - openai_val = secret_value(self.openai_api_key) - if not openai_val or is_placeholder(openai_val): - auto_discover = self.llm.auto_discover_openai and self.auto_discover_openai - if auto_discover: - key, source = discover_openai_api_key() - if key and not is_placeholder(key): # key is raw str from discovery - self.openai_api_key = SecretStr(key) - self.llm.openai_api_key = SecretStr(key) - logger.debug(f"OpenAI API key discovered from: {source}") - else: - logger.debug("OpenAI API key auto-discovery is disabled") - - # Anthropic API key auto-discovery - anthropic_val = secret_value(self.anthropic_api_key) - if not anthropic_val or is_placeholder(anthropic_val): - auto_discover = self.llm.auto_discover_anthropic and self.auto_discover_anthropic - if auto_discover: - key, source = discover_anthropic_api_key() - if key and not is_placeholder(key): # key is raw str from discovery - self.anthropic_api_key = SecretStr(key) - self.llm.anthropic_api_key = SecretStr(key) - logger.debug(f"Anthropic API key discovered from: {source}") - else: - logger.debug("Anthropic API key auto-discovery is disabled") - - # Gemini API key auto-discovery - gemini_val = secret_value(self.gemini_api_key) - if not gemini_val or is_placeholder(gemini_val): - auto_discover = self.llm.auto_discover_gemini and self.auto_discover_gemini - if auto_discover: - key, source = discover_gemini_api_key() - if key and not is_placeholder(key): # key is raw str from discovery - self.gemini_api_key = SecretStr(key) - self.llm.gemini_api_key = SecretStr(key) - logger.debug(f"Gemini API key discovered from: {source}") - elif source != "not found": - logger.debug(f"Gemini: {source}") - else: - logger.debug("Gemini API key auto-discovery is disabled") - - return self - - # Global settings instance settings = Settings() diff --git a/src/codespy/config_dspy.py b/src/codespy/config_dspy.py index daadb79..f302125 100644 --- a/src/codespy/config_dspy.py +++ b/src/codespy/config_dspy.py @@ -1,11 +1,12 @@ """DSPy signatures configuration and environment variable handling.""" import logging -import os -from typing import Any, Literal +from typing import Literal from pydantic import BaseModel, Field +from codespy.config_io import DEFAULT_EXCLUDED_DIRECTORIES, OutputFormat + logger = logging.getLogger(__name__) @@ -30,16 +31,6 @@ class RLMFallbackConfig(BaseModel): predict_threshold: float = Field(default=0.50, ge=0.0, le=1.0) -class MemorySignatureConfig(BaseModel): - """Per-signature Hippocampus memory overrides. - - All fields are optional — ``None`` means "use the global memory default" - (see ``codespy.config_memory.MemoryConfig``). - """ - - enabled: bool | None = None # _MEMORY_ENABLED - - class SignatureConfig(BaseModel): """Configuration for a single signature.""" @@ -53,141 +44,65 @@ class SignatureConfig(BaseModel): scan_unchanged: bool | None = None # For supply_chain: scan unmodified artifacts/manifests skip_refinement_when_clean: bool | None = None # For scope: skip LLM refinement when 1 scope, 0 orphans - memory: MemorySignatureConfig = Field(default_factory=MemorySignatureConfig) - - -# Known signature names for env var routing -SIGNATURE_NAMES = { - "code_review", - "doc", - "scope", - "supply_chain", - "summary", - "audit", -} -# Create uppercase prefixes for matching (e.g., "CODE_REVIEW_", "SUPPLY_CHAIN_") -SIGNATURE_PREFIXES = {name.upper() + "_": name for name in SIGNATURE_NAMES} - -# Known signature settings for validation, derived from the models so the env -# var routing can never drift from the declared fields. ``memory`` is excluded -# because it is nested and routed via _MEMORY_ instead. -SIGNATURE_SETTINGS = set(SignatureConfig.model_fields) - {"memory"} - -# Known per-signature memory settings, routed via _MEMORY_ -MEMORY_SIGNATURE_SETTINGS = set(MemorySignatureConfig.model_fields) +class ReviewConfig(BaseModel): + """Configuration for the review subsystem. + Contains per-signature configs and top-level review settings. + """ -# Env var name (without RLM_FALLBACK_ prefix) -> RLMFallbackConfig field name. -RLM_FALLBACK_ENV_SETTINGS = { - "ENABLED": "enabled", - "REACT_THRESHOLD": "react_threshold", - "CHAIN_OF_THOUGHT_THRESHOLD": "chain_of_thought_threshold", - "PREDICT_THRESHOLD": "predict_threshold", + # Minimum confidence threshold for reported issues + min_confidence: float = Field(default=0.81, ge=0.0, le=1.0) + + # Output settings + output_format: OutputFormat = "markdown" + output_stdout: bool = True # Enable stdout output + output_git: bool = True # Enable Git platform review comments + + # Cache directory + cache_dir: str = "~/.cache/codespy" + + # File exclusion settings + excluded_directories: list[str] = Field(default=DEFAULT_EXCLUDED_DIRECTORIES) + + # Per-signature configs + supply_chain: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(model="bedrock/converse/nvidia.nemotron-super-3-120b") + ) + code_review: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(model="bedrock/converse/global.anthropic.claude-opus-5-5") + ) + doc: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(max_iters=2, max_llm_calls=4, model="bedrock/converse/global.anthropic.claude-opus-5-5") + ) + scope: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(max_iters=3, max_llm_calls=5, model="bedrock/converse/global.anthropic.claude-opus-5-5") + ) + summary: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(max_iters=1, max_llm_calls=2, model="bedrock/converse/nvidia.nemotron-super-3-120b") + ) + audit: SignatureConfig = Field( + default_factory=lambda: SignatureConfig(max_iters=1, max_llm_calls=2, model="bedrock/converse/global.anthropic.claude-opus-5-5") + ) + + def signatures(self) -> dict[str, SignatureConfig]: + """Return a dict of signature name -> SignatureConfig.""" + return { + "supply_chain": self.supply_chain, + "code_review": self.code_review, + "doc": self.doc, + "scope": self.scope, + "summary": self.summary, + "audit": self.audit, + } + + +# Known signature names derived from ReviewConfig +SIGNATURE_NAMES = set(ReviewConfig.model_fields.keys()) - { + "min_confidence", + "output_format", + "output_stdout", + "output_git", + "cache_dir", + "excluded_directories", } - - -def apply_rlm_fallback_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - """Apply RLM_FALLBACK_* environment variable overrides. - - Maps: RLM_FALLBACK_REACT_THRESHOLD=0.30 -> rlm_fallback.react_threshold - """ - from dotenv import dotenv_values - - env_vars = {**dotenv_values(".env"), **os.environ} - - for key, value in env_vars.items(): - if value is None: - continue - key_upper = key.upper() - if not key_upper.startswith("RLM_FALLBACK_"): - continue - remainder = key_upper[len("RLM_FALLBACK_") :] - field = RLM_FALLBACK_ENV_SETTINGS.get(remainder) - if field is None: - continue - rlm_config = config.setdefault("rlm_fallback", {}) - if not isinstance(rlm_config, dict): - continue - rlm_config[field] = convert_env_value(value) - - return config - - -def convert_env_value(value: str) -> Any: - """Convert environment variable string to appropriate Python type.""" - import json - - if value.lower() in ("true", "false"): - return value.lower() == "true" - elif value.isdigit(): - return int(value) - elif value.startswith("[") or value.startswith("{"): - try: - return json.loads(value) - except json.JSONDecodeError: - return value - elif value.lower() == "null" or value == "": - return None - return value - - -def apply_signature_env_overrides(config: dict[str, Any]) -> dict[str, Any]: - """Apply environment variable overrides to config for signature settings. - - Handles three patterns: - - ``CODE_REVIEW_MAX_ITERS`` -> signatures.code_review.max_iters - - ``SUPPLY_CHAIN_ENABLED`` -> signatures.supply_chain.enabled - - ``CODE_REVIEW_MEMORY_ENABLED`` -> signatures.code_review.memory.enabled - - Top-level settings (DEFAULT_MODEL, AWS_REGION, MEMORY_DEFAULT_ENABLED, etc.) - are handled directly by pydantic-settings and should NOT be processed here. - """ - # Load .env file first to ensure env vars are available - from dotenv import dotenv_values - - env_vars = {**dotenv_values(".env"), **os.environ} # .env + actual env vars - - for key, value in env_vars.items(): - if value is None: - continue - key_upper = key.upper() - - # Match signature prefix - signature_name = None - remainder = None - for prefix, sig_name in SIGNATURE_PREFIXES.items(): - if key_upper.startswith(prefix): - signature_name = sig_name - remainder = key_upper[len(prefix) :] - break - - if not signature_name or remainder is None: - continue - - # Nested memory setting: _MEMORY_ - if remainder.startswith("MEMORY_"): - memory_setting = remainder[len("MEMORY_") :].lower() - if memory_setting not in MEMORY_SIGNATURE_SETTINGS: - continue - if "signatures" not in config: - config["signatures"] = {} - if signature_name not in config["signatures"]: - config["signatures"][signature_name] = {} - if "memory" not in config["signatures"][signature_name]: - config["signatures"][signature_name]["memory"] = {} - sig_memory = config["signatures"][signature_name]["memory"] - sig_memory[memory_setting] = convert_env_value(value) - continue - - # Flat signature setting - setting = remainder.lower() - if setting not in SIGNATURE_SETTINGS: - continue - if "signatures" not in config: - config["signatures"] = {} - if signature_name not in config["signatures"]: - config["signatures"][signature_name] = {} - config["signatures"][signature_name][setting] = convert_env_value(value) - - return config diff --git a/src/codespy/config_llm.py b/src/codespy/config_llm.py index e728f21..8517d71 100644 --- a/src/codespy/config_llm.py +++ b/src/codespy/config_llm.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, Field, SecretStr +from codespy.config_dspy import ReasoningEffort, RLMFallbackConfig from codespy.config_utils import secret_value logger = logging.getLogger(__name__) @@ -275,44 +276,20 @@ class LLMConfig(BaseModel): azure_api_base: str | None = None azure_api_version: str | None = None - # Enable provider-side prompt caching (Anthropic, OpenAI, Bedrock, etc.) - enable_prompt_caching: bool = True - - def sync_from_flat( - self, - *, - openai_api_key: SecretStr | None = None, - anthropic_api_key: SecretStr | None = None, - gemini_api_key: SecretStr | None = None, - aws_region: str | None = None, - aws_access_key_id: SecretStr | None = None, - aws_secret_access_key: SecretStr | None = None, - ) -> dict[str, object]: - """Sync flat settings into this LLMConfig and return values to propagate back. - - Priority: flat fields (from env vars via pydantic-settings) > nested defaults. - Returns dict of field values that should be set on the parent Settings. - """ - # Step 1: Flat → nested (env vars win over nested defaults) - if secret_value(openai_api_key): - self.openai_api_key = openai_api_key - if secret_value(anthropic_api_key): - self.anthropic_api_key = anthropic_api_key - if secret_value(gemini_api_key): - self.gemini_api_key = gemini_api_key - if aws_region: - self.aws_region = aws_region - if secret_value(aws_access_key_id): - self.aws_access_key_id = aws_access_key_id - if secret_value(aws_secret_access_key): - self.aws_secret_access_key = aws_secret_access_key - - # Step 2: Return merged values (nested fills gaps where flat is not set) - return { - "openai_api_key": openai_api_key if secret_value(openai_api_key) else self.openai_api_key, - "anthropic_api_key": anthropic_api_key if secret_value(anthropic_api_key) else self.anthropic_api_key, - "gemini_api_key": gemini_api_key if secret_value(gemini_api_key) else self.gemini_api_key, - "aws_access_key_id": aws_access_key_id if secret_value(aws_access_key_id) else self.aws_access_key_id, - "aws_secret_access_key": aws_secret_access_key if secret_value(aws_secret_access_key) else self.aws_secret_access_key, - "enable_prompt_caching": self.enable_prompt_caching, - } + # Default model settings + default_model: str = "bedrock/converse/nvidia.nemotron-nano-3-30b" + extraction_model: str | None = "bedrock/converse/nvidia.nemotron-nano-3-30b" # TwoStepAdapter extraction (falls back to default_model) + default_max_iters: int = 5 + default_max_llm_calls: int = 8 + default_reasoning_effort: ReasoningEffort = "medium" + default_temperature: float = 1.0 + default_max_tokens: int = 32000 + + # Global LLM reliability settings + retries: int = 2 # LLM_RETRIES + timeout: int = 240 # LLM_TIMEOUT + + # RLM fallback configuration + rlm_fallback: RLMFallbackConfig = Field(default_factory=RLMFallbackConfig) + + diff --git a/src/codespy/config_memory.py b/src/codespy/config_memory.py index 2268a71..1bf77b8 100644 --- a/src/codespy/config_memory.py +++ b/src/codespy/config_memory.py @@ -1,49 +1,196 @@ -"""Memory (Hippocampus) configuration and storage factory.""" +"""Memory (Hippocampus, Cerebral, Prefrontal) configuration and storage factory.""" from __future__ import annotations import logging import os - -from typing import TYPE_CHECKING, Any +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Literal from pydantic import BaseModel, Field from codespy.config_dspy import ReasoningEffort +# Hindsight constraint: validate_retain_completion_token_budget requires +# retain_max_completion_tokens (default 64000) > retain_chunk_size. +# If chunk_size >= 64000, update_bank_config rejects the update and the bank +# silently falls back to defaults (chunk 3000, no mission). +_HINDSIGHT_RETAIN_MAX_COMPLETION_TOKENS = 64000 + +# Hindsight reads its reflect-loop iteration cap from this env var once, when +# its config is first built (at import). See get_cerebral(). +HINDSIGHT_REFLECT_MAX_ITERATIONS_ENV = "HINDSIGHT_API_REFLECT_MAX_ITERATIONS" + +# Hindsight reads its reflect context token cap from this env var once, when +# its config is first built. Also bounds briefing refreshes (LOW budget). +HINDSIGHT_REFLECT_MAX_CONTEXT_TOKENS_ENV = "HINDSIGHT_API_REFLECT_MAX_CONTEXT_TOKENS" + +# Default bank ID for memory storage when not explicitly configured. +DEFAULT_BANK_ID = "codebase" + +# Fixed schema name for episodic memory (Hippocampus). +EPISODIC_SCHEMA = "episodic" + if TYPE_CHECKING: + from codespy.agents.memory.cerebral import Cerebral from codespy.agents.memory.postgres import EpisodeStore + from codespy.agents.memory.prefrontal import Prefrontal from codespy.config import Settings +# Known signature names for memory gating checks +from codespy.config_dspy import SIGNATURE_NAMES + logger = logging.getLogger(__name__) +# Default embedding model per LLM provider prefix. Used by get_cerebral() +# when embeddings.model is None. litellm-sdk routes through litellm. +EMBEDDING_MODELS: dict[str, str] = { + "bedrock": "bedrock/cohere.embed-v4:0", + "openai": "openai/text-embedding-3-small", + "anthropic": "openai/text-embedding-3-small", + "gemini": "gemini/text-embedding-004", + "azure": "azure/text-embedding-3-small", + "litellm": "openai/text-embedding-3-small", +} + + class ReflectionModuleConfig(BaseModel): """LLM overrides for a single reflection module (Distiller / Cartographer). All fields are optional — ``None`` means "fall back to the corresponding - top-level ``default_*`` setting" (see ``codespy.config.Settings``). + top-level ``llm.default_*`` setting" (see ``codespy.config.Settings``). The reflection modules are compact summarize/curate tasks rather than deep analysis, so they are good candidates for a cheaper model tier than the one used for code review. """ - model: str | None = None # MEMORY__MODEL - reasoning_effort: ReasoningEffort | None = None # MEMORY__REASONING_EFFORT - temperature: float | None = None # MEMORY__TEMPERATURE - max_tokens: int | None = None # MEMORY__MAX_TOKENS - max_iters: int | None = 1 # MEMORY__MAX_ITERS - max_llm_calls: int | None = 2 # MEMORY__MAX_LLM_CALLS + model: str | None = "bedrock/converse/nvidia.nemotron-super-3-120b" + reasoning_effort: ReasoningEffort | None = None + temperature: float | None = None + max_tokens: int | None = None + max_iters: int | None = 1 + max_llm_calls: int | None = 2 + + +class HippocampusConfig(BaseModel): + """Hippocampus (episodic memory) configuration.""" + + # Whether to apply head+tail trajectory bounding before distillation. + compact_trajectory: bool = True + + # Token budgets + max_hippocampus_tokens: int = Field(default=16384) + max_hippocampus_item_tokens: int = Field(default=512) + max_trajectory_tokens: int | None = Field(default=16384) + max_question_tokens: int | None = Field(default=8192) + + # Reflection modules + distiller: ReflectionModuleConfig = Field(default_factory=lambda: ReflectionModuleConfig(model="bedrock/converse/nvidia.nemotron-super-3-120b")) + cartographer: ReflectionModuleConfig = Field(default_factory=lambda: ReflectionModuleConfig(model="bedrock/converse/nvidia.nemotron-super-3-120b")) + + +class CerebralRetainConfig(BaseModel): + """Cerebral (semantic memory) LLM configuration for fact extraction.""" + + model: str | None = "bedrock/converse/nvidia.nemotron-super-3-120b" # Falls back to llm.default_model + # Value is in chars. The merged observations blob is bounded by + # max_hippocampus_tokens (~65K chars worst case), so large blobs split + # into several chunks. Must be < _HINDSIGHT_RETAIN_MAX_COMPLETION_TOKENS. + chunk_size: int = Field(default=12288, gt=0, lt=_HINDSIGHT_RETAIN_MAX_COMPLETION_TOKENS) + + +class CerebralConsolidationConfig(BaseModel): + """Cerebral (semantic memory) LLM configuration for consolidation. + + Consolidation merges facts after episode retention. Falls back to + cerebral.retain.model → llm.default_model when unset. + """ + + model: str | None = "bedrock/converse/nvidia.nemotron-super-3-120b" # Falls back to cerebral.retain.model → llm.default_model + # Hindsight observation_scope_limits cap. -1 = unlimited, 0 = no new observations. + # Applied per scope as: scope=[org:*, repo:*, project_scope:*], limit=N + max_observations_per_scope: int = Field(default=100, ge=-1) + + +class MentalModelsConfig(BaseModel): + """Mental-model (briefing) refresh LLM configuration. + + Mental-model refresh synthesizes briefings after consolidation. + Falls back to prefrontal.recall.model → cerebral.retain.model → llm.default_model + when unset. Only used when reflects > 0. + + Delta mode means nothing is lost - the refresh covers all new facts since + the last successful refresh when the interval expires. + """ + + model: str | None = "bedrock/converse/nvidia.nemotron-super-3-120b" # Falls back to prefrontal.recall.model → retain → default + max_tokens: int = Field(default=2048, gt=0) # Briefing size + min_refresh_seconds: int = Field(default=0, ge=0) # Minimum seconds between refreshes + + +class CerebralEmbeddingsConfig(BaseModel): + """Cerebral (semantic memory) embeddings configuration.""" + + model: str | None = "bedrock/cohere.embed-v4:0" # Auto-derived from provider if unset + max_input_chars: int | None = Field( + default=None, + ge=0, + description="Maximum characters per embedding input. null=auto (2048 for Bedrock Cohere v3), 0=off, N=cap", + ) + + +class CerebralConfig(BaseModel): + """Cerebral (semantic memory) configuration.""" + + retain: CerebralRetainConfig = Field(default_factory=CerebralRetainConfig) + consolidation: CerebralConsolidationConfig = Field(default_factory=CerebralConsolidationConfig) + mental_models: MentalModelsConfig = Field(default_factory=MentalModelsConfig) + embeddings: CerebralEmbeddingsConfig = Field(default_factory=CerebralEmbeddingsConfig) + + +class RecallConfig(BaseModel): + """Prefrontal recall (read-time context injection) configuration. + + Controls the read-time recall into agent context. Only used when + memory.prefrontal.reflects > 0. + """ + + # Model for Hindsight reflect (pre-call context and recall_memory tool). + # Falls back to cerebral.retain.model → llm.default_model. + # Unused when reflects=0 (no LLM at read time). + model: str | None = "bedrock/converse/nvidia.nemotron-super-3-120b" + # local: this scope/repo only. org: also other repos of the same owner. + # bank: every repo in the bank (crosses organisations — opt-in only). + reach: Literal["local", "org", "bank"] = "org" + # Token budget for the pre-call context (local facets + remote facts). + max_tokens: int = Field(default=8192, gt=0) + # Token budget for recall_memory tool results. + max_tool_tokens: int = Field(default=2048, gt=0) + # recall_memory tool call limit (0 = tool disabled). + max_tool_calls: int = Field(default=0, ge=0) + + +class PrefrontalConfig(BaseModel): + """Prefrontal (semantic recall into agent context) configuration.""" + + # 0: raw facts only, no LLM at read time and no briefings (no mental models). + # N > 0: Hindsight reflect loop, capped at N iterations (both Prefrontal and + # briefing refreshes). Uses Hindsight's LOW budget (0.5× multiplier), so the + # global cap is set to 2× reflects to achieve exactly reflects iterations. + reflects: int = Field(default=3, ge=0) + # Recall settings (read-time context injection). + recall: RecallConfig = Field(default_factory=RecallConfig) class LLMSettings(BaseModel): """Fully resolved LLM settings for one named unit of work. Produced by ``Settings.get_llm_config()`` for either a signature - (``signatures.``) or a reflection module (``memory.``): every - field is either the name-specific override or the corresponding top-level - default, so consumers never re-apply fallback logic. + (``review.``) or a reflection module (``memory.hippocampus.`` for + ``memory_``): every field is either the name-specific override or the + corresponding top-level default, so consumers never re-apply fallback logic. """ model: str @@ -58,24 +205,25 @@ class LLMSettings(BaseModel): 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 = "episodic" # MEMORY_POSTGRES_SCHEMA (search_path per memory type; None = public) + + host: str | None = None + port: int = 5432 + user: str | None = None + password: str | None = None + database: str = "codespy" def build_uri(self) -> str | None: """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. + Each memory store (EpisodeStore for episodic, Cerebral for semantic) + handles its own schema internally and sets search_path accordingly. + The URI returned here is schema-less so stores can share the same + base connection while using 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 return f"postgresql://{cred}@{self.host}:{self.port}/{self.database}" @@ -83,209 +231,82 @@ def build_uri(self) -> str | None: 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 + + name: str = "codespy" + port: int | None = None + data_dir: str | None = None class MemoryConfig(BaseModel): - """Global memory (Hippocampus) configuration. + """Global memory (Hippocampus + Cerebral) configuration. - Controls where episodes are persisted and the default memory knob - applied to every agent. Per-signature ``memory:`` blocks override the - ``default_*`` values. + Controls where episodes are persisted and the memory knob applied to + every agent. """ # PostgreSQL connection settings postgres: PostgresConfig = Field(default_factory=PostgresConfig) pg0: Pg0Config = Field(default_factory=Pg0Config) - bank_id: str | None = None # MEMORY_BANK_ID (defaults to "codespy") + bank_id: str | None = None - # Memory default — overridable per-signature - default_enabled: bool = False # MEMORY_DEFAULT_ENABLED + # Master switch + enabled: bool = False - # Whether to apply head+tail trajectory bounding before distillation. - # When false, the full trajectory goes to the Distiller and ContextSafe - # RLM fallback handles overflow if it exceeds the model's context window. - compact_trajectory: bool = True # MEMORY_COMPACT_TRAJECTORY - - # Ceiling on the rendered ContextMemory. This is the *persisted* artifact and it - # is prepended to every predictor of the wrapped agent, so it is re-sent on - # every ReAct iteration (~default_max_iters times per scope) plus once per - # reflection call. Easily the most cost-sensitive of the three budgets. - # Approximate item capacity is max_context_memory_tokens divided by - # max_context_item_tokens (16384 / 512 = 32 items). - # MEMORY_MAX_CONTEXT_MEMORY_TOKENS - max_context_memory_tokens: int = Field(default=16384) - - # Per-item ceiling handed to the Distiller/Cartographer as a prompt input, so - # they keep each context-memory item compact instead of spending the whole memory - # budget on one verbose entry. Soft limit: it is expressed to the LLM rather - # than enforced in code (truncating an item could corrupt an exact constant). - # The hard, memory-wide limit is max_context_memory_tokens, enforced by the - # Evictor. MEMORY_MAX_CONTEXT_ITEM_TOKENS - 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 - # then sends it twice. 16384 is ~12% of a 128k window and preserves both the - # orientation steps (60% head) and the conclusions (40% tail). - max_trajectory_tokens: int | None = 16384 # MEMORY_MAX_TRAJECTORY_TOKENS - - # Head+tail cap on the serialized agent inputs used as the Distiller/Cartographer - # "question". Only applies when the caller passes no 'question': otherwise - # every input field is serialized, which for code review means the full patch - # of every changed file. See Hippocampus.max_question_tokens. - max_question_tokens: int | None = 8192 # MEMORY_MAX_QUESTION_TOKENS - - # Per-module LLM overrides for the reflection pipeline. - # Unset fields fall back to the top-level ``default_*`` settings. - distiller: ReflectionModuleConfig = Field( - default_factory=ReflectionModuleConfig - ) # MEMORY_DISTILLER_* - cartographer: ReflectionModuleConfig = Field( - default_factory=ReflectionModuleConfig - ) # MEMORY_CARTOGRAPHER_* - - -# Env var suffix (after MEMORY_POSTGRES_) -> PostgresConfig field name. -POSTGRES_ENV_SETTINGS = { - "HOST": "host", - "PORT": "port", - "USER": "user", - "PASSWORD": "password", - "DATABASE": "database", - "SCHEMA": "schema", -} + # Hippocampus (episodic memory) configuration + hippocampus: HippocampusConfig = Field(default_factory=HippocampusConfig) -# Env var suffix (after MEMORY_PG0_) -> Pg0Config field name. -PG0_ENV_SETTINGS = { - "NAME": "name", - "PORT": "port", - "DATA_DIR": "data_dir", -} + # Cerebral (semantic memory) configuration + cerebral: CerebralConfig = Field(default_factory=CerebralConfig) -# 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 = { - "BANK_ID": "bank_id", - "DEFAULT_ENABLED": "default_enabled", - "COMPACT_TRAJECTORY": "compact_trajectory", - "MAX_CONTEXT_MEMORY_TOKENS": "max_context_memory_tokens", - "MAX_CONTEXT_ITEM_TOKENS": "max_context_item_tokens", - "MAX_TRAJECTORY_TOKENS": "max_trajectory_tokens", - "MAX_QUESTION_TOKENS": "max_question_tokens", -} + # Prefrontal (semantic recall into agent context) configuration + prefrontal: PrefrontalConfig = Field(default_factory=PrefrontalConfig) -# The reflection modules, derived from the MemoryConfig fields that hold a + +# Memory unit name prefix for LLM work units +MEMORY_UNIT_PREFIX = "memory_" + +# Memory unit names (used for cost buckets, config lookup, and logging) +MEMORY_DISTILLER = "memory_distiller" +MEMORY_CARTOGRAPHER = "memory_cartographer" +MEMORY_RETAIN = "memory_retain" +MEMORY_EMBEDDINGS = "memory_embeddings" +MEMORY_OTHER = "memory_other" +MEMORY_RECALL = "memory_recall" +MEMORY_CONSOLIDATION = "memory_consolidation" +MEMORY_MENTAL_MODELS = "memory_mental_models" + +# The reflection modules, derived from the HippocampusConfig fields that hold a # ReflectionModuleConfig. Iterate this instead of hardcoding module names so # adding a new reflection module only requires declaring its field above. +# Names are prefixed with MEMORY_UNIT_PREFIX to match the config/env naming. REFLECTION_MODULES: tuple[str, ...] = tuple( - name - for name, field in MemoryConfig.model_fields.items() + f"{MEMORY_UNIT_PREFIX}{name}" + for name, field in HippocampusConfig.model_fields.items() if field.annotation is ReflectionModuleConfig ) -# Env var suffix -> ReflectionModuleConfig field name, routed via -# MEMORY__ (e.g. MEMORY_DISTILLER_MODEL). -REFLECTION_MODULE_ENV_SETTINGS = { - name.upper(): name for name in ReflectionModuleConfig.model_fields -} - -# 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: - """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_POSTGRES_HOST=myhost -> memory.postgres.host - MEMORY_DEFAULT_ENABLED=true -> memory.default_enabled - MEMORY_MAX_CONTEXT_MEMORY_TOKENS=512 -> memory.max_context_memory_tokens - - Nested sub-model overrides use a second level of nesting:: - MEMORY_DISTILLER_MODEL=... -> memory.distiller.model - MEMORY_CARTOGRAPHER_TEMPERATURE=0 -> memory.cartographer.temperature +def reflection_module_config(hippocampus: HippocampusConfig, name: str) -> ReflectionModuleConfig: + """Get the ReflectionModuleConfig for a prefixed module name. - 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). - - - Note: ``_MEMORY_*`` vars are handled separately by - ``apply_signature_env_overrides`` and are ignored here, since they never - match a bare ``MEMORY_`` prefix. + Removes the MEMORY_UNIT_PREFIX and returns the corresponding config + from the HippocampusConfig. Args: - config: The YAML-derived config dict to mutate. + hippocampus: The HippocampusConfig instance. + name: The prefixed module name (e.g., "memory_distiller"). Returns: - The same dict, with ``memory`` overrides applied. - """ - from dotenv import dotenv_values - - from codespy.config_dspy import convert_env_value - - env_vars = {**dotenv_values(".env"), **os.environ} - - for key, value in env_vars.items(): - if value is None: - continue - key_upper = key.upper() - if not key_upper.startswith("MEMORY_"): - continue - remainder = key_upper[len("MEMORY_") :] - - memory_config = config.setdefault("memory", {}) - if not isinstance(memory_config, dict): - continue - - # 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. - nested = next( - ( - (field, settings_map, remainder[len(prefix):]) - for prefix, (field, settings_map) in NESTED_ENV_PREFIXES.items() - if remainder.startswith(prefix) - ), - None, - ) - if nested is not None: - field, settings_map, setting = nested - setting_field = settings_map.get(setting) - if setting_field is None: - continue - sub_config = memory_config.setdefault(field, {}) - if not isinstance(sub_config, dict): - continue - sub_config[setting_field] = convert_env_value(value) - continue - - field = MEMORY_ENV_SETTINGS.get(remainder) - if field is None: - continue - memory_config[field] = convert_env_value(value) + The ReflectionModuleConfig for that module. - return config + Raises: + ValueError: If the name doesn't start with the expected prefix. + AttributeError: If the module doesn't exist on HippocampusConfig. + """ + if not name.startswith(MEMORY_UNIT_PREFIX): + raise ValueError(f"Expected name to start with '{MEMORY_UNIT_PREFIX}', got: {name}") + field_name = name[len(MEMORY_UNIT_PREFIX):] + return getattr(hippocampus, field_name) # Cached singleton store. Avoids reconstructing the EpisodeStore's connection pool @@ -319,8 +340,8 @@ def get_episode_store(settings: Settings) -> EpisodeStore | None: return _store mem = settings.memory - bank_id = mem.bank_id or _generate_bank_id() - schema = mem.postgres.schema # "episodic" by default + bank_id = mem.bank_id or DEFAULT_BANK_ID + schema = EPISODIC_SCHEMA # Try external PostgreSQL first uri = mem.postgres.build_uri() @@ -377,12 +398,9 @@ def verify_memory_access(settings: Settings) -> tuple[bool, str]: Tuple of (success, message). Success is True when memory is disabled (no active signatures use it) or when the storage backend responds. """ - from codespy.config_dspy import SIGNATURE_NAMES - - # Skip if no enabled signature uses memory - if not any( - settings.is_signature_enabled(sig) and settings.get_memory_enabled(sig) - for sig in SIGNATURE_NAMES + # Skip if memory disabled or no enabled signature uses memory + if not settings.memory.enabled or not any( + settings.is_signature_enabled(sig) for sig in SIGNATURE_NAMES ): return True, "Memory disabled — skipping storage check" @@ -398,4 +416,591 @@ def verify_memory_access(settings: Settings) -> tuple[bool, str]: except Exception as e: return False, f"Memory storage not accessible: {e}" - return True, f"Memory storage verified (PostgreSQL, bank={settings.memory.bank_id or _generate_bank_id()})" + return True, f"Memory storage verified (PostgreSQL, bank={settings.memory.bank_id or DEFAULT_BANK_ID})" + + +# Cached singleton Cerebral instance. +_cerebral: "Cerebral" | None = None +_cerebral_built = False + + +def _litellm_credentials(settings: "Settings", model: str) -> tuple[str | None, str | None]: + """Derive API key and base_url from a litellm model string. + + All Cerebral LLM calls go through litellm. This function parses the litellm + model string and extracts credentials based on the provider prefix. + + Args: + settings: Application settings containing LLM credentials. + model: Full litellm model string (e.g., "bedrock/converse/moonshotai.kimi-k2.5"). + + Returns: + ``(api_key, base_url)`` - None values indicate no special handling. + """ + # Parse litellm model string: "provider/model_path" + parts = model.split("/", 1) + provider = parts[0] if len(parts) > 1 else "openai" + + llm = settings.llm + api_key: str | None = None + base_url: str | None = None + + if provider == "bedrock": + pass # Uses AWS env vars (AWS_ACCESS_KEY_ID, etc.) - litellm reads these + elif provider == "openai": + api_key = llm.openai_api_key.get_secret_value() if llm.openai_api_key else None + base_url = llm.openai_api_base + elif provider == "anthropic": + api_key = llm.anthropic_api_key.get_secret_value() if llm.anthropic_api_key else None + elif provider == "gemini": + api_key = llm.gemini_api_key.get_secret_value() if llm.gemini_api_key else None + elif provider in ("azure", "azure_ai"): + api_key = llm.azure_api_key.get_secret_value() if llm.azure_api_key else None + base_url = llm.azure_api_base + # Azure API version comes from env var AZURE_API_VERSION, same as DSPy + # For any other provider, let litellm resolve from env/globals + + return api_key, base_url + + +def _cerebral_litellm_params(settings: "Settings") -> tuple[str, str | None, str | None]: + """Derive MemoryEngine litellm params from the cerebral model config + LLM credentials. + + Convenience wrapper around _litellm_credentials for the retain model. + + Returns: + ``(model, api_key, base_url)`` - model is the full litellm string unchanged. + """ + llm_config = settings.get_llm_config(MEMORY_RETAIN) + model = llm_config.model + api_key, base_url = _litellm_credentials(settings, model) + return model, api_key, base_url + + +def get_cerebral(settings: "Settings") -> "Cerebral" | None: + """Return the cached Cerebral instance, or None if unavailable. + + Cerebral activates unconditionally (like ``get_episode_store``). + Agent-level ``settings.memory.enabled`` handles gating. + LLM parameters are auto-derived from the ``cerebral.retain`` + config model string and ``settings.llm`` credentials. + + The store is built once and cached (module-level singleton). + + Call :func:`reset_cerebral` after changing settings (e.g. via + ``reload_settings``) to force a rebuild on next access. + + Args: + settings: Application settings. + + Returns: + Cached Cerebral instance, or None when hindsight-api-slim is + not installed or PostgreSQL is not available. + """ + global _cerebral, _cerebral_built + if _cerebral_built: + return _cerebral + + try: + from codespy.agents.memory.cerebral import Cerebral + except ImportError: + logger.warning( + "Memory is enabled but hindsight-api-slim is not installed. " + "Semantic memory disabled. Install with: pip install hindsight-api-slim" + ) + _cerebral = None + _cerebral_built = True + return None + + pg_uri = settings.memory.postgres.build_uri() + if not pg_uri: + try: + from codespy.agents.memory.pg0_manager import get_pg0_uri + + pg_uri = get_pg0_uri( + name=settings.memory.pg0.name, + port=settings.memory.pg0.port, + data_dir=settings.memory.pg0.data_dir, + ) + except Exception: + logger.warning("Memory enabled but no PostgreSQL available for Cerebral") + _cerebral = None + _cerebral_built = True + return None + + model, api_key, base_url = _cerebral_litellm_params(settings) + bank_id = settings.memory.bank_id or DEFAULT_BANK_ID + # Choose embedding default based on the provider prefix + provider_prefix = model.split("/", 1)[0] if "/" in model else "openai" + embeddings_model = ( + settings.memory.cerebral.embeddings.model + or EMBEDDING_MODELS.get(provider_prefix, "openai/text-embedding-3-small") + ) + + # Prefrontal recall model (optional) + recall_model = settings.memory.prefrontal.recall.model + reflect_kwargs: dict[str, str | None] = {} + if recall_model: + recall_api_key, recall_base_url = _litellm_credentials(settings, recall_model) + reflect_kwargs = { + "reflect_llm_model": recall_model, + "reflect_llm_api_key": recall_api_key, + "reflect_llm_base_url": recall_base_url, + } + + # Consolidation model (optional) - pass to Cerebral only when explicitly set + # and no operator env override is present (env wins over config) + cons_model = settings.memory.cerebral.consolidation.model + consolidation_kwargs: dict[str, str | None] = {} + if cons_model and not os.environ.get("HINDSIGHT_API_CONSOLIDATION_LLM_MODEL"): + cons_api_key, cons_base_url = _litellm_credentials(settings, cons_model) + consolidation_kwargs = { + "consolidation_llm_model": cons_model, + "consolidation_llm_api_key": cons_api_key, + "consolidation_llm_base_url": cons_base_url, + } + + # Apply Hindsight LLM defaults BEFORE Cerebral construction (these are read + # during MemoryEngine.__init__). reflect_config is applied after construction. + _apply_hindsight_llm_call_defaults(settings) + + # Apply mental-model refresh LLM config to Hindsight raw config BEFORE Cerebral construction + _apply_mental_model_refresh_llm(settings) + + # Extraction model for two-step structured output fallback + extraction_model = settings.get_llm_config(MEMORY_RETAIN).extraction_model + extraction_kwargs: dict[str, str | None] = {} + if extraction_model: + extraction_api_key, extraction_base_url = _litellm_credentials(settings, extraction_model) + extraction_kwargs = { + "extraction_llm_model": extraction_model, + "extraction_llm_api_key": extraction_api_key, + "extraction_llm_base_url": extraction_base_url, + } + + try: + _cerebral = Cerebral( + database_url=pg_uri, + llm_provider="litellm", + llm_model=model, + llm_api_key=api_key, + llm_base_url=base_url, + bank_id=bank_id, + embeddings_model=embeddings_model, + retain_chunk_size=settings.memory.cerebral.retain.chunk_size, + mental_models=settings.memory.prefrontal.reflects > 0, + max_mental_model_tokens=settings.memory.cerebral.mental_models.max_tokens, + embeddings_max_input_chars=settings.memory.cerebral.embeddings.max_input_chars, + min_mental_model_refresh_seconds=settings.memory.cerebral.mental_models.min_refresh_seconds, + max_observations_per_scope=settings.memory.cerebral.consolidation.max_observations_per_scope, + **reflect_kwargs, + **consolidation_kwargs, + **extraction_kwargs, + ) + except Exception: + logger.error( + "Cerebral initialization FAILED (bank=%s, schema=semantic). " + "Semantic memory is disabled for this run.", + bank_id, + exc_info=True, + ) + _cerebral = None + _cerebral_built = True + return None + + reflect_model_str = f", reflect_model={recall_model}" if recall_model else "" + consolidation_model_str = ( + f", consolidation_model={consolidation_kwargs.get('consolidation_llm_model')}" + if consolidation_kwargs.get("consolidation_llm_model") + else "" + ) + mental_models_model_str = ( + f", mental_models_model={settings.memory.cerebral.mental_models.model}" + if settings.memory.cerebral.mental_models.model and settings.memory.prefrontal.reflects > 0 + else "" + ) + logger.info( + "Cerebral initialized (bank=%s, provider=litellm, model=%s%s%s%s, schema=semantic)", + bank_id, + model, + reflect_model_str, + consolidation_model_str, + mental_models_model_str, + ) + # Apply reflect caps programmatically (after Cerebral construction, before first use) + _apply_reflect_config(settings.memory.prefrontal) + _cerebral_built = True + return _cerebral + + +# Inverse of Hindsight's LOW budget multiplier (0.5) to double the raw +# iteration cap so both Prefrontal and briefing refresh get exactly +# ``reflects`` iterations: max(1, int(2*reflects * 0.5)) = reflects. +# See memory_engine.py:15134-15138 for the multiplier logic. +_LOW_BUDGET_MULTIPLIER_INVERSE = 2 + + +def _apply_hindsight_llm_call_defaults(settings: "Settings") -> None: + """Apply Hindsight LLM timeout and retry settings from codespy config. + + Sets global ``llm_timeout`` and ``llm_max_retries`` so all Hindsight operations + (retain, consolidation, reflect, mental-model refresh) inherit codespy's + ``llm.timeout`` / ``llm.retries``. Per-operation env vars still win automatically + because Hindsight prefers non-None per-op values. + + Also sets ``reflect_llm_timeout`` / ``reflect_llm_max_retries`` to override + Hindsight's 30s reflect default when no operator env is set. + + Operator env (HINDSIGHT_API_LLM_TIMEOUT, HINDSIGHT_API_LLM_MAX_RETRIES, + HINDSIGHT_API_REFLECT_LLM_TIMEOUT, HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES) wins. + + Called in get_cerebral() after Cerebral import but before Cerebral() construction. + This must run before Cerebral is built because the defaults are resolved in + MemoryEngine.__init__. + + Args: + settings: Application settings. + """ + try: + import hindsight_api.config as ha_cfg + + raw = ha_cfg._get_raw_config() + except Exception as e: + logger.warning("Could not access Hindsight raw config for LLM defaults: %s", e) + return + + # --- Global settings (retain, consolidation, reflect, mental-model refresh) --- + + # Determine global timeout: env wins, then settings.llm.timeout + global_timeout: float | None = None + global_env_timeout = os.environ.get("HINDSIGHT_API_LLM_TIMEOUT") + if global_env_timeout is not None: + try: + global_timeout = float(global_env_timeout) + logger.info("Using env HINDSIGHT_API_LLM_TIMEOUT=%s (operator override)", global_timeout) + except ValueError: + pass + if global_timeout is None and hasattr(raw, "llm_timeout"): + global_timeout = float(settings.llm.timeout) + raw.llm_timeout = global_timeout + + # Determine global retries: env wins, then settings.llm.retries + global_retries: int | None = None + global_env_retries = os.environ.get("HINDSIGHT_API_LLM_MAX_RETRIES") + if global_env_retries is not None: + try: + global_retries = int(global_env_retries) + logger.info("Using env HINDSIGHT_API_LLM_MAX_RETRIES=%s (operator override)", global_retries) + except ValueError: + pass + if global_retries is None and hasattr(raw, "llm_max_retries"): + global_retries = int(settings.llm.retries) + raw.llm_max_retries = global_retries + + # --- Reflect-specific settings (override Hindsight's 30s default) --- + + # Determine reflect timeout: env wins, then settings.llm.timeout + reflect_timeout: float | None = None + reflect_env_timeout = os.environ.get("HINDSIGHT_API_REFLECT_LLM_TIMEOUT") or os.environ.get("HINDSIGHT_API_LLM_TIMEOUT") + if reflect_env_timeout is not None: + try: + reflect_timeout = float(reflect_env_timeout) + # Log the actual env var that was used + used_env = "HINDSIGHT_API_REFLECT_LLM_TIMEOUT" if os.environ.get("HINDSIGHT_API_REFLECT_LLM_TIMEOUT") else "HINDSIGHT_API_LLM_TIMEOUT" + logger.info("Using env %s=%s (operator override)", used_env, reflect_timeout) + except ValueError: + pass + if reflect_timeout is None and hasattr(raw, "reflect_llm_timeout"): + reflect_timeout = float(settings.llm.timeout) + raw.reflect_llm_timeout = reflect_timeout + + # Determine reflect retries: env wins, then settings.llm.retries + reflect_retries: int | None = None + reflect_env_retries = os.environ.get("HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES") or os.environ.get("HINDSIGHT_API_LLM_MAX_RETRIES") + if reflect_env_retries is not None: + try: + reflect_retries = int(reflect_env_retries) + # Log the actual env var that was used + used_env = "HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES" if os.environ.get("HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES") else "HINDSIGHT_API_LLM_MAX_RETRIES" + logger.info("Using env %s=%s (operator override)", used_env, reflect_retries) + except ValueError: + pass + if reflect_retries is None and hasattr(raw, "reflect_llm_max_retries"): + reflect_retries = int(settings.llm.retries) + raw.reflect_llm_max_retries = reflect_retries + + # Summary log + global_timeout_str = str(global_timeout) if global_timeout is not None else "-" + global_retries_str = str(global_retries) if global_retries is not None else "-" + reflect_timeout_str = str(reflect_timeout) if reflect_timeout is not None else "-" + reflect_retries_str = str(reflect_retries) if reflect_retries is not None else "-" + + if global_timeout is not None or global_retries is not None: + logger.info( + "Hindsight LLM defaults: timeout=%s, max_retries=%s (reflect timeout=%s, max_retries=%s)", + global_timeout_str, + global_retries_str, + reflect_timeout_str, + reflect_retries_str, + ) + + +def _apply_reflect_config(cfg: PrefrontalConfig) -> dict[str, Any]: + """Apply reflect caps to Hindsight config programmatically. + + Called in get_cerebral() after Cerebral import. Hindsight re-reads + reflect_max_iterations at call time (not in _CONFIGURABLE_FIELDS), so + setting it on _get_raw_config() after import takes effect. + + The global cap is set to 2 × reflects (when reflects > 0) so that with + Hindsight's LOW budget multiplier (0.5×), both Prefrontal and briefing + refresh get exactly ``reflects`` iterations. + + Args: + cfg: PrefrontalConfig with reflects. + + Returns: + Dict of effective values applied (iterations). Empty dict when reflects == 0. + """ + # Set a minimal cap of 2 even when reflects == 0 to bound any Hindsight reflect + raw_cap = max(2 * max(cfg.reflects, 1), 1) + + try: + import hindsight_api.config as ha_cfg + + raw = ha_cfg._get_raw_config() + except Exception as e: + logger.warning("Could not access Hindsight raw config: %s", e) + return {} + + applied: dict[str, Any] = {} + + # Apply iteration cap: env wins as-is (operator raw cap), else double reflects + env_iters = os.environ.get(HINDSIGHT_REFLECT_MAX_ITERATIONS_ENV) + if env_iters is not None: + try: + applied["iterations"] = int(env_iters) + applied["budget"] = "low" + logger.info("Using env %s=%s (operator override)", HINDSIGHT_REFLECT_MAX_ITERATIONS_ENV, env_iters) + except ValueError: + pass + elif cfg.reflects > 0: + if hasattr(raw, "reflect_max_iterations"): + raw.reflect_max_iterations = raw_cap + applied["iterations"] = raw_cap + applied["budget"] = "low" + + # Hindsight's reflect_max_context_tokens default is 100000; we no longer + # override it. The default is used which allows full context without + # triggering split synthesis. + + if applied: + effective_iters = applied.get("iterations", 0) + # With LOW budget (0.5×), iterations = max(1, int(cap * 0.5)) + effective_reflects = max(1, int(effective_iters * 0.5)) + logger.info( + "Prefrontal reflect caps: iterations=%d (hindsight cap=%d, budget=low) context_tokens=100000", + effective_reflects, + effective_iters, + ) + + return applied + + +def get_prefrontal( + settings: Settings, + task_name: str, + repo_full_name: str | None, + scope_topic_ids: Sequence[str] | None = None, + include_repo: bool = False, +) -> Prefrontal | None: + """Return a Prefrontal for one agent call, or None when inactive. + + Active only when ``settings.memory.enabled`` is true, a repo is + known and ``get_cerebral`` returns a Cerebral. Otherwise the agent + behaves exactly as without Prefrontal. Never raises. + """ + try: + if not settings.memory.enabled or not repo_full_name: + return None + cerebral = get_cerebral(settings) + if cerebral is None: + return None + from codespy.agents.memory.prefrontal import Prefrontal + + return Prefrontal( + settings.memory.prefrontal, + cerebral, + task_name, + repo_full_name, + scope_topic_ids=scope_topic_ids, + include_repo=include_repo, + ) + except Exception: + logger.warning("Prefrontal unavailable for %s", task_name, exc_info=True) + return None + + +# Consumer signatures that share the run-level Prefrontal load +_RUN_PREFRONTAL_CONSUMERS = ("summary", "code_review", "doc", "supply_chain", "audit") + + +def get_run_prefrontal( + settings: Settings, + repo_full_name: str, + scope_topic_ids: Sequence[str], +) -> Prefrontal | None: + """Return a Prefrontal for the shared run-level recall, or None when inactive. + + Active only when: + - Memory is enabled globally (settings.memory.enabled) + - A repo is known + - Cerebral is available + - At least one consumer (summary, code_review, doc, supply_chain, audit) + has is_signature_enabled True + + When active, returns a Prefrontal with task_name="review", include_repo=True, + and all scope_topic_ids. The run-level recall is shared by all consumer agents. + + Never raises (same soft-fail behavior as get_prefrontal). + """ + try: + if not settings.memory.enabled or not repo_full_name: + return None + + # Check if any consumer has memory enabled + has_memory_consumer = any( + settings.is_signature_enabled(sig) + for sig in _RUN_PREFRONTAL_CONSUMERS + ) + if not has_memory_consumer: + return None + + cerebral = get_cerebral(settings) + if cerebral is None: + return None + + from codespy.agents.memory.prefrontal import Prefrontal + + return Prefrontal( + settings.memory.prefrontal, + cerebral, + task_name="review", + repo_full_name=repo_full_name, + scope_topic_ids=scope_topic_ids, + include_repo=True, + ) + except Exception: + logger.warning("Run Prefrontal unavailable for %s", repo_full_name, exc_info=True) + return None + + +# Snapshot of raw config mental_model_refresh fields before mutation, +# restored by reset_cerebral() to avoid leaking settings across rebuilds. +_mental_model_refresh_snapshot: dict[str, Any] | None = None + + +def _apply_mental_model_refresh_llm(settings: "Settings") -> None: + """Apply mental-model refresh LLM config to Hindsight raw config programmatically. + + Hindsight reads mental_model_refresh_llm_* from raw config (populated from + HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_* env vars). This function sets those + fields when codespy's config has an explicit mental_models.model and no env + override is present. + + The snapshot is stored in _mental_model_refresh_snapshot so reset_cerebral() + can restore the original values. + + Args: + settings: Application settings. + """ + global _mental_model_refresh_snapshot + + # Only apply when reflects > 0 and a mental_models model is configured + # reflects gates mental models; the model itself lives in cerebral.mental_models + if settings.memory.prefrontal.reflects == 0: + return + mm_cfg = settings.memory.cerebral.mental_models + if not mm_cfg.model: + return + + # Operator env wins - if any HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_* is set, + # do not touch the raw config (the env values are already there) + env_vars = [ + "HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_PROVIDER", + "HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_MODEL", + "HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_API_KEY", + "HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_BASE_URL", + ] + if any(os.environ.get(v) for v in env_vars): + logger.info("Using HINDSIGHT_API_MENTAL_MODEL_REFRESH_LLM_* env vars (operator override)") + return + + try: + import hindsight_api.config as ha_cfg + + raw = ha_cfg._get_raw_config() + except Exception as e: + logger.warning("Could not access Hindsight raw config for mental-model refresh: %s", e) + return + + # Snapshot current values before mutation + _mental_model_refresh_snapshot = {} + for field in ("mental_model_refresh_llm_provider", "mental_model_refresh_llm_model", + "mental_model_refresh_llm_api_key", "mental_model_refresh_llm_base_url"): + if hasattr(raw, field): + _mental_model_refresh_snapshot[field] = getattr(raw, field) + + # Get credentials for the configured model + model = mm_cfg.model + api_key, base_url = _litellm_credentials(settings, model) + + try: + # Set the raw config fields (guarded by hasattr for forward compatibility) + if hasattr(raw, "mental_model_refresh_llm_provider"): + raw.mental_model_refresh_llm_provider = "litellm" + if hasattr(raw, "mental_model_refresh_llm_model"): + raw.mental_model_refresh_llm_model = model + if hasattr(raw, "mental_model_refresh_llm_api_key"): + raw.mental_model_refresh_llm_api_key = api_key + if hasattr(raw, "mental_model_refresh_llm_base_url"): + # Use "" for no base URL (not None), so Hindsight doesn't inherit reflect's base URL + raw.mental_model_refresh_llm_base_url = base_url or "" + + logger.info("Mental-model refresh LLM: model=%s", model) + except Exception as e: + logger.warning("Failed to set mental-model refresh LLM config: %s", e) + + +def reset_cerebral() -> None: + """Clear the cached Cerebral instance so it is rebuilt on next access. + + Call this after reloading settings (e.g. ``reload_settings()``) so a + changed ``memory`` configuration takes effect. + + Also restores the Hindsight raw config mental_model_refresh fields from + the snapshot taken by _apply_mental_model_refresh_llm to avoid leaking + settings across rebuilds. + """ + global _cerebral, _cerebral_built, _mental_model_refresh_snapshot + + if _cerebral is not None: + try: + _cerebral.close() + except Exception: + pass + _cerebral = None + _cerebral_built = False + + # Restore raw config snapshot if present + if _mental_model_refresh_snapshot: + try: + import hindsight_api.config as ha_cfg + + raw = ha_cfg._get_raw_config() + for field, value in _mental_model_refresh_snapshot.items(): + if hasattr(raw, field): + setattr(raw, field, value) + except Exception: + pass + _mental_model_refresh_snapshot = None diff --git a/src/codespy/config_utils.py b/src/codespy/config_utils.py index 559e7a3..d7ffe72 100644 --- a/src/codespy/config_utils.py +++ b/src/codespy/config_utils.py @@ -2,7 +2,12 @@ from __future__ import annotations -from pydantic import SecretStr +import json +import os +from typing import Any + +from dotenv import dotenv_values +from pydantic import BaseModel, SecretStr def secret_value(s: SecretStr | None) -> str: @@ -19,3 +24,182 @@ def secret_value(s: SecretStr | None) -> str: if s is None: return "" return s.get_secret_value() + + +def convert_env_value(value: str) -> Any: + """Convert environment variable string to appropriate Python type. + + Conversion rules: + - Empty string: treated as unset (return None to signal skip) + - Literal "null": returns Python None (explicit reset) + - JSON array/object: parsed via json.loads + - "true"/"false": converted to bool + - Other values: returned as raw string (let pydantic coerce) + """ + # Empty string means unset - skip this value + if value == "": + return None # Signal to skip + + # Literal null means explicit reset to None + if value.lower() == "null": + return None + + # JSON arrays/objects + if value.startswith("[") or value.startswith("{"): + try: + return json.loads(value) + except json.JSONDecodeError: + return value + + # Boolean values + if value.lower() in ("true", "false"): + return value.lower() == "true" + + # Return raw string - pydantic will coerce int/float as needed + return value + + +def build_env_map( + sections: dict[str, type[BaseModel]], + *, + collapsed_paths: set[tuple[str, ...]] | None = None, + full_name_paths: set[tuple[str, ...]] | None = None, +) -> dict[str, tuple[str, ...]]: + """Build a mapping of env var name -> config path tuple. + + Args: + sections: Dict of section name -> BaseModel subclass (e.g., {"llm": LLMConfig}) + collapsed_paths: Set of path prefixes to collapse (drop the prefix segments). + For example, {("llm",)} drops "LLM_" from all descendants of llm. + full_name_paths: Set of full paths that keep their full name (not collapsed). + These take precedence over collapsed_paths. For example, + {("llm", "retries")} keeps "LLM_RETRIES" instead of "RETRIES". + + Returns: + Dict mapping env var name to tuple path (e.g., {"DEFAULT_MODEL": ("llm", "default_model")}) + + Examples: + >>> build_env_map( + ... sections={"llm": LLMConfig, "review": ReviewConfig}, + ... collapsed_paths={("llm",), ("memory", "hippocampus"), ("memory", "cerebral")}, + ... full_name_paths={("llm", "retries"), ("llm", "timeout")}, + ... ) + # Produces: + # { + # "DEFAULT_MODEL": ("llm", "default_model"), # llm prefix dropped + # "LLM_RETRIES": ("llm", "retries"), # full_name_paths keeps LLM_ + # "OPENAI_API_KEY": ("llm", "openai_api_key"), # credentials are bare + # "REVIEW_SCOPE_ENABLED": ("review", "scope", "enabled"), # review prefix kept + # "MEMORY_DISTILLER_MODEL": ("memory", "hippocampus", "distiller", "model"), + # } + """ + env_map: dict[str, tuple[str, ...]] = {} + collapsed_paths = collapsed_paths or set() + full_name_paths = full_name_paths or set() + + for section_name, model_cls in sections.items(): + _build_env_map_for_model( + model_cls, + prefix=(section_name,), + collapsed_paths=collapsed_paths, + full_name_paths=full_name_paths, + env_map=env_map, + ) + + return env_map + + +def _build_env_map_for_model( + model_cls: type[BaseModel], + prefix: tuple[str, ...], + collapsed_paths: set[tuple[str, ...]], + full_name_paths: set[tuple[str, ...]], + env_map: dict[str, tuple[str, ...]], +) -> None: + """Recursively build env map for a model and its nested models.""" + for field_name, field_info in model_cls.model_fields.items(): + annotation = field_info.annotation + + # Use alias if available, otherwise field name + seg = field_info.alias or field_name + p = prefix + (seg,) + + # Determine the env var name + if p in full_name_paths: + # Use full path (keep all segments) + env_name = "_".join(p).upper() + else: + # Apply collapsing: drop each segment for which the path up to that segment is in collapsed_paths + segments = [] + for i, segment in enumerate(p): + if p[: i + 1] not in collapsed_paths: + segments.append(segment) + env_name = "_".join(segments).upper() + + # Check if this is a nested BaseModel + origin = getattr(annotation, "__origin__", None) + if origin is not None: + # Handle Optional[X] or List[X] etc - unwrap + args = getattr(annotation, "__args__", ()) + if args: + annotation = args[0] + + # Check if the unwrapped annotation is a BaseModel subclass + try: + if isinstance(annotation, type) and issubclass(annotation, BaseModel): + # Recurse into nested model, but don't add to env_map directly + _build_env_map_for_model( + annotation, + prefix=p, + collapsed_paths=collapsed_paths, + full_name_paths=full_name_paths, + env_map=env_map, + ) + continue + except TypeError: + pass + + # This is a leaf field - add to map with duplicate check + if env_name in env_map: + old_path = env_map[env_name] + if old_path != p: + raise ValueError(f"Duplicate env var {env_name}: {old_path} vs {p}") + # Same path, already added - skip + else: + env_map[env_name] = p + + +def apply_env_overrides(config: dict[str, Any], env_map: dict[str, tuple[str, ...]]) -> dict[str, Any]: + """Apply environment variable overrides to a config dict. + + Args: + config: The YAML-derived config dict to mutate. + env_map: Mapping of env var name -> config path tuple. + + Returns: + The same dict, with env overrides applied. + """ + # Load .env file first, then actual env vars (env vars win) + env_vars = {**dotenv_values(".env"), **os.environ} + + for env_name, path in env_map.items(): + value = env_vars.get(env_name) + if value is None: + continue + + converted = convert_env_value(value) + if converted is None and value != "null": + # Empty string - skip + continue + + # Navigate/create the nested dict structure + target = config + for key in path[:-1]: + if key not in target or target[key] is None: + target[key] = {} + target = target[key] + + # Set the value + target[path[-1]] = converted + + return config diff --git a/src/codespy/tools/cyber/osv/server.py b/src/codespy/tools/cyber/osv/server.py index f37dd4c..1caea0f 100644 --- a/src/codespy/tools/cyber/osv/server.py +++ b/src/codespy/tools/cyber/osv/server.py @@ -5,14 +5,14 @@ from functools import lru_cache from typing import Any -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.cyber.osv.client import OSVClient logger = logging.getLogger(__name__) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("osv") +mcp = MCPServer("osv") _client: OSVClient | None = None @@ -225,9 +225,5 @@ def scan_cargo_package(name: str, version: str) -> dict[str, Any]: if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - _client = OSVClient() mcp.run() diff --git a/src/codespy/tools/git/base.py b/src/codespy/tools/git/base.py index b2350e9..3690a09 100644 --- a/src/codespy/tools/git/base.py +++ b/src/codespy/tools/git/base.py @@ -1,6 +1,7 @@ """Abstract base class for Git platform clients.""" from abc import ABC, abstractmethod +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -9,6 +10,18 @@ from codespy.tools.git.models import PullRequest +@dataclass +class SubmittedReview: + """Reference to a submitted review for later editing. + + Attributes: + id: The review/note ID from the platform + body_suffix: Any content appended to the body (e.g., fallback inline comments) + """ + id: int + body_suffix: str = "" + + class GitClient(ABC): """Abstract base class for Git platform clients (GitHub, GitLab).""" @@ -81,7 +94,7 @@ def submit_review( body: str, comments: list[dict] | None = None, commit_sha: str | None = None, - ) -> None: + ) -> SubmittedReview | None: """Submit a review on a pull request. Args: @@ -92,6 +105,26 @@ def submit_review( - line: Line number - body: Comment text commit_sha: Commit SHA to review (defaults to head SHA) + + Returns: + SubmittedReview with the review ID and any body suffix appended, + or None if no review was created. + """ + ... + + @abstractmethod + def update_review( + self, + url: str, + review: SubmittedReview, + body: str, + ) -> None: + """Update an existing review with a new body. + + Args: + url: Pull request URL + review: The SubmittedReview returned by submit_review + body: New review body (replaces the original) """ ... diff --git a/src/codespy/tools/git/github_client.py b/src/codespy/tools/git/github_client.py index 1a20966..681eca1 100644 --- a/src/codespy/tools/git/github_client.py +++ b/src/codespy/tools/git/github_client.py @@ -9,7 +9,7 @@ from github.PullRequest import PullRequest as GHPullRequest from codespy.config_utils import secret_value -from codespy.tools.git.base import GitClient +from codespy.tools.git.base import GitClient, SubmittedReview from codespy.tools.git.models import ( ChangedFile, FileStatus, @@ -148,7 +148,7 @@ def clone_repository( """ # Determine target directory if target_path is None: - cache_dir = self.settings.cache_dir + cache_dir = self.settings.review.cache_dir cache_dir.mkdir(parents=True, exist_ok=True) repo_dir = cache_dir / owner / repo_name else: @@ -216,7 +216,7 @@ def submit_review( body: str, comments: list[dict] | None = None, commit_sha: str | None = None, - ) -> None: + ) -> SubmittedReview | None: """Submit a review on a pull request. Uses optimistic approach: try all comments first, then validate and retry on failure. @@ -229,7 +229,10 @@ def submit_review( - line: Line number (in the diff, not the file) - body: Comment text - side: 'RIGHT' for additions, 'LEFT' for deletions (default: RIGHT) - commit_sha: Commit SHA to review (defaults to PR head SHA) + commit_sha: Commit SHA to review (defaults to head SHA) + + Returns: + SubmittedReview with the review ID and body suffix. Note: Uses event='COMMENT' to avoid approving/requesting changes. @@ -280,14 +283,18 @@ def submit_review( review_comment["start_line"] = comment["start_line"] review_comments.append(review_comment) + body_suffix = "" + review_id = 0 + # Try to submit the review optimistically with all inline comments try: - gh_pr.create_review( + review = gh_pr.create_review( commit=repo.get_commit(review_commit), body=body, event="COMMENT", comments=review_comments, ) + review_id = review.id logger.info( f"Submitted review on {owner}/{repo_name}#{pr_number} " f"with {len(review_comments)} inline comments" @@ -325,12 +332,14 @@ def submit_review( # Retry with valid comments only try: - gh_pr.create_review( + review = gh_pr.create_review( commit=repo.get_commit(review_commit), body=updated_body, event="COMMENT", comments=valid_comments, ) + review_id = review.id + body_suffix = updated_body[len(body):] logger.info( f"Submitted review on {owner}/{repo_name}#{pr_number} " f"with {len(valid_comments)} inline comments (after validation)" @@ -342,12 +351,14 @@ def submit_review( ) all_failed_comments = review_comments # All original comments final_body = self._append_comments_to_body(body, all_failed_comments) - gh_pr.create_review( + review = gh_pr.create_review( commit=repo.get_commit(review_commit), body=final_body, event="COMMENT", comments=[], ) + review_id = review.id + body_suffix = final_body[len(body):] logger.info( f"Submitted review on {owner}/{repo_name}#{pr_number} " f"(body only, all comments)" @@ -358,6 +369,30 @@ def submit_review( if skipped_path_comments: logger.info(f"Skipped {len(skipped_path_comments)} comments for files not in PR") + return SubmittedReview(id=review_id, body_suffix=body_suffix) + + def update_review( + self, + url: str, + review: SubmittedReview, + body: str, + ) -> None: + """Update an existing GitHub PR review with a new body. + + Args: + url: GitHub PR URL + review: The SubmittedReview returned by submit_review + body: New review body (replaces the original) + """ + owner, repo_name, pr_number = self.parse_url(url) + repo = self.github.get_repo(f"{owner}/{repo_name}") + gh_pr: GHPullRequest = repo.get_pull(pr_number) + + # Get the review and edit it + gh_review = gh_pr.get_review(review.id) + gh_review.edit(body=body) + logger.info(f"Updated review {review.id} on {owner}/{repo_name}#{pr_number}") + def _append_comments_to_body(self, body: str, comments: list[dict]) -> str: """Append inline comments to the review body when they can't be posted inline. diff --git a/src/codespy/tools/git/gitlab_client.py b/src/codespy/tools/git/gitlab_client.py index 72dcbbf..bff2c50 100644 --- a/src/codespy/tools/git/gitlab_client.py +++ b/src/codespy/tools/git/gitlab_client.py @@ -8,7 +8,7 @@ from git import Repo from codespy.config_utils import secret_value -from codespy.tools.git.base import GitClient +from codespy.tools.git.base import GitClient, SubmittedReview from codespy.tools.git.models import ( ChangedFile, FileStatus, @@ -223,7 +223,7 @@ def clone_repository( """ # Determine target directory if target_path is None: - cache_dir = self.settings.cache_dir + cache_dir = self.settings.review.cache_dir cache_dir.mkdir(parents=True, exist_ok=True) # Handle nested namespaces repo_dir = cache_dir / owner.replace("/", "_") / repo_name @@ -285,7 +285,7 @@ def submit_review( body: str, comments: list[dict] | None = None, commit_sha: str | None = None, - ) -> None: + ) -> SubmittedReview | None: """Submit a review on a merge request. GitLab doesn't have a "review" concept like GitHub. Instead: @@ -302,6 +302,9 @@ def submit_review( - line: Line number - body: Comment text commit_sha: Commit SHA to review (defaults to head SHA) + + Returns: + SubmittedReview with the note ID and body suffix, or None if no note was created. """ project_path = self._get_project_path(url) _, _, pr_number = self.parse_url(url) @@ -407,8 +410,12 @@ def submit_review( logger.info(f"Moving {len(failed_comments)} comments to body (could not post inline)") # Post main review body as a note (with any failed comments appended) + note_id = 0 + body_suffix = "" if final_body: - gl_mr.notes.create({"body": final_body}) + note = gl_mr.notes.create({"body": final_body}) + note_id = note.id + body_suffix = final_body[len(body):] logger.info(f"Posted review on {project_path}!{pr_number}") if comments: @@ -417,6 +424,35 @@ def submit_review( f"inline comments on {project_path}!{pr_number}" ) + if note_id: + return SubmittedReview(id=note_id, body_suffix=body_suffix) + return None + + def update_review( + self, + url: str, + review: SubmittedReview, + body: str, + ) -> None: + """Update an existing GitLab MR note with a new body. + + Args: + url: GitLab MR URL + review: The SubmittedReview returned by submit_review + body: New review body (replaces the original) + """ + project_path = self._get_project_path(url) + _, _, pr_number = self.parse_url(url) + + project = self.gitlab_client.projects.get(project_path) + gl_mr = project.mergerequests.get(pr_number) + + # Get the note and update it + note = gl_mr.notes.get(review.id) + note.body = body + note.save() + logger.info(f"Updated note {review.id} on {project_path}!{pr_number}") + def _append_comments_to_body(self, body: str, comments: list[dict]) -> str: """Append inline comments to the review body when they can't be posted inline. diff --git a/src/codespy/tools/git/patch_utils.py b/src/codespy/tools/git/patch_utils.py deleted file mode 100644 index 2af0e4d..0000000 --- a/src/codespy/tools/git/patch_utils.py +++ /dev/null @@ -1,438 +0,0 @@ -"""Patch compaction utilities to expand diff context to function boundaries.""" - -from __future__ import annotations - -import logging -import re -from pathlib import Path -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from codespy.agents.review.scope.models import ScopeResult - -from codespy.tools.git.models import ChangedFile, FileStatus -from codespy.tools.parsers.treesitter import FunctionInfo, TreeSitterParser - -logger = logging.getLogger(__name__) - -# Non-code file extensions that should not be processed -NON_CODE_EXTENSIONS = { - "md", - "txt", - "rst", - "yaml", - "yml", - "json", - "toml", - "ini", - "cfg", - "conf", - "xml", - "html", - "htm", - "css", - "scss", - "sass", - "less", - "csv", - "tsv", -} - -# Compiled regex for hunk header parsing -HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@") - - -def compact_patches(scopes: list[ScopeResult], repo_path: Path) -> None: - """Compact patches on all changed files across scopes (mutates in place). - - For each code file with a patch, attempts compaction. Non-code files - and files where compaction fails keep their original patch. - - Args: - scopes: List of scope results containing changed files - repo_path: Path to the cloned repository - """ - parser = TreeSitterParser(repo_path) - - for scope in scopes: - for file in scope.changed_files: - if not _should_compact_file(file): - continue - - try: - if file.patch is None: - continue - compacted = compact_patch(file.patch, Path(file.filename), repo_path, parser) - if compacted != file.patch: - logger.debug(f"Compacted patch for {file.filename}") - file.patch = compacted - except Exception as e: - logger.debug(f"Failed to compact patch for {file.filename}: {e}") - # Keep original patch on failure - - -def _should_compact_file(file: ChangedFile) -> bool: - """Check if a file should be compacted. - - Args: - file: The ChangedFile to check - - Returns: - True if the file should be compacted - """ - # Skip files without patches - if not file.patch: - return False - - # Skip deleted files (no source to read) - if file.status == FileStatus.REMOVED: - return False - - # Skip non-code files - if file.extension in NON_CODE_EXTENSIONS: - return False - - # Skip binary and lock files - return not (file.is_binary or file.is_lock_file) - - -def compact_patch( - raw_patch: str, - file_path: Path, - repo_path: Path, - parser: TreeSitterParser | None = None, -) -> str: - """Expand diff context to full function bodies using TreeSitter. - - Returns compacted patch string, or raw_patch unchanged if compaction - isn't possible (no TreeSitter, not a code file, deleted file, etc.). - - Args: - raw_patch: The original unified diff patch - file_path: Path to the file (relative to repo root) - repo_path: Path to the repository root - parser: Optional TreeSitterParser instance (created if not provided) - - Returns: - Compacted patch or original patch if compaction fails - """ - # Skip empty patches - if not raw_patch: - return raw_patch - - # Create parser if not provided - if parser is None: - parser = TreeSitterParser(repo_path) - - # Check if TreeSitter is available - if not parser.available: - logger.debug(f"TreeSitter not available, skipping compaction for {file_path}") - return raw_patch - - # Check if file is a code file we can parse - extension = file_path.suffix.lstrip(".") if file_path.suffix else "" - if not extension or extension in NON_CODE_EXTENSIONS: - return raw_patch - - # Get absolute file path - abs_file_path = repo_path / file_path - if not abs_file_path.exists(): - logger.warning(f"Source file not found: {abs_file_path}") - return raw_patch - - # Find function definitions in the file - try: - functions = parser.find_function_definitions(abs_file_path) - except Exception as e: - logger.debug(f"Failed to find functions in {file_path}: {e}") - return raw_patch - - if not functions: - # No functions found, nothing to expand - return raw_patch - - # Parse hunks from patch - hunks = _parse_hunks(raw_patch) - if not hunks: - return raw_patch - - # Read source file lines - try: - source_lines = abs_file_path.read_text().splitlines() - except Exception as e: - logger.debug(f"Failed to read source file {file_path}: {e}") - return raw_patch - - # Expand each hunk to function boundaries - expanded_hunks: list[dict[str, Any]] = [] - for hunk in hunks: - expanded = _expand_hunk_to_functions(hunk, functions, source_lines) - if expanded: - expanded_hunks.append(expanded) - - if not expanded_hunks: - return raw_patch - - # Merge overlapping hunks - merged_hunks = _merge_hunks(expanded_hunks) - - # Rebuild the compacted patch - compacted = _rebuild_patch(raw_patch, merged_hunks, source_lines) - - return compacted if compacted else raw_patch - - -def _parse_hunks(patch: str) -> list[dict[str, Any]]: - """Parse hunks from a unified diff patch. - - Args: - patch: The unified diff patch string - - Returns: - List of hunk dictionaries with parsed info - """ - hunks = [] - lines = patch.split("\n") - i = 0 - - while i < len(lines): - line = lines[i] - - # Check for hunk header - match = HUNK_HEADER_RE.match(line) - if match: - old_start = int(match.group(1)) - old_count = int(match.group(2)) if match.group(2) else 1 - new_start = int(match.group(3)) - new_count = int(match.group(4)) if match.group(4) else 1 - - # Collect hunk lines - hunk_lines = [] - i += 1 - changed_new_lines = [] - current_new_line = new_start - - while i < len(lines): - hunk_line = lines[i] - - # Stop at next hunk header or empty line that ends the hunk - if hunk_line.startswith("@@"): - break - - hunk_lines.append(hunk_line) - - # Track changed lines in the new file - if hunk_line.startswith("+"): - changed_new_lines.append(current_new_line) - current_new_line += 1 - elif hunk_line.startswith(" "): - current_new_line += 1 - elif hunk_line.startswith("-"): - pass # Deleted line, not in new file - elif hunk_line.startswith("\\"): - # "\ No newline at end of file" marker - pass - - i += 1 - - hunks.append( - { - "header": line, - "old_start": old_start, - "old_count": old_count, - "new_start": new_start, - "new_count": new_count, - "lines": hunk_lines, - "changed_new_lines": changed_new_lines, - } - ) - else: - i += 1 - - return hunks - - -def _expand_hunk_to_functions( - hunk: dict[str, Any], - functions: list[FunctionInfo], - source_lines: list[str], -) -> dict[str, Any] | None: - """Expand a hunk to cover enclosing function boundaries. - - Args: - hunk: The hunk dictionary - functions: List of function definitions - source_lines: Source file lines - - Returns: - Expanded hunk dictionary or None if no expansion needed - """ - changed_lines = hunk["changed_new_lines"] - if not changed_lines: - return hunk - - # Find enclosing functions for changed lines - enclosing_functions: list[FunctionInfo] = [] - for line_num in changed_lines: - # Find innermost enclosing function - best_match: FunctionInfo | None = None - for func in functions: - if func.line_start <= line_num <= func.line_end and ( - best_match is None - or ( - func.line_start >= best_match.line_start - and func.line_end <= best_match.line_end - ) - ): - best_match = func - - if best_match and best_match not in enclosing_functions: - enclosing_functions.append(best_match) - - if not enclosing_functions: - # No enclosing functions, return hunk unchanged - return hunk - - # Calculate expansion range - min_func_start = min(f.line_start for f in enclosing_functions) - max_func_end = max(f.line_end for f in enclosing_functions) - - # Determine hunk boundaries in new file - hunk_start_new = hunk["new_start"] - hunk_end_new = hunk_start_new + hunk["new_count"] - 1 - # Account for lines that don't end with newline - if hunk["lines"] and hunk["lines"][-1].startswith("\\"): - hunk_end_new = hunk_start_new + hunk["new_count"] - - # Don't expand if already covering the entire function(s) - if min_func_start >= hunk_start_new and max_func_end <= hunk_end_new: - return hunk - - # Calculate expansion - expansion_start = min(hunk_start_new, min_func_start) - expansion_end = max(hunk_end_new, max_func_end) - - return { - "original_hunk": hunk, - "expansion_start": expansion_start, - "expansion_end": expansion_end, - "hunk_start_new": hunk_start_new, - "hunk_end_new": hunk_end_new, - "enclosing_functions": enclosing_functions, - } - - -def _merge_hunks(expanded_hunks: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Merge overlapping or adjacent expanded hunks. - - Args: - expanded_hunks: List of expanded hunk dictionaries - - Returns: - List of merged hunks - """ - if len(expanded_hunks) <= 1: - return expanded_hunks - - # Sort by expansion start (fallback to new_start for non-expanded hunks) - sorted_hunks = sorted( - expanded_hunks, key=lambda h: h.get("expansion_start", h.get("new_start", 0)) - ) - - merged = [sorted_hunks[0]] - - for hunk in sorted_hunks[1:]: - last = merged[-1] - - # Check if this hunk overlaps or is adjacent to the last merged hunk - last_end = last.get("expansion_end", 0) - current_start = hunk.get("expansion_start", 0) - - # Only merge expanded hunks (those with expansion boundaries) - can_merge = "expansion_start" in last and "expansion_start" in hunk - if can_merge and current_start <= last_end + 1: # Overlapping or adjacent - # Merge: extend the end if needed - last["expansion_end"] = max(last_end, hunk.get("expansion_end", 0)) - # Keep track of original hunks for reconstruction - if "original_hunk" in last and "original_hunk" in hunk: - if "merged_hunks" not in last: - last["merged_hunks"] = [last["original_hunk"]] - last["merged_hunks"].append(hunk["original_hunk"]) - else: - merged.append(hunk) - - return merged - - -def _build_expanded_hunk( - merged_hunk: dict[str, Any], - source_lines: list[str], -) -> tuple[str, list[str]]: - """Build the header and content lines for one expanded hunk. - - Returns: - Tuple of (hunk_header, hunk_lines). - """ - expansion_start = merged_hunk.get("expansion_start") - expansion_end = merged_hunk.get("expansion_end") - original_hunk = merged_hunk.get("original_hunk", merged_hunk) - - if expansion_start is None or expansion_end is None: - return original_hunk["header"], original_hunk["lines"] - - hunk_start_new = merged_hunk.get("hunk_start_new", expansion_start) - hunk_end_new = merged_hunk.get("hunk_end_new", expansion_end) - - new_hunk_lines: list[str] = [] - - # Pre-context - for line_num in range(expansion_start, hunk_start_new): - if line_num <= len(source_lines): - new_hunk_lines.append(f" {source_lines[line_num - 1]}") - - # Diff lines from constituent hunks - for sub_hunk in merged_hunk.get("merged_hunks", [original_hunk]): - new_hunk_lines.extend(sub_hunk.get("lines", [])) - - # Post-context - for line_num in range(hunk_end_new + 1, expansion_end + 1): - if line_num <= len(source_lines): - new_hunk_lines.append(f" {source_lines[line_num - 1]}") - - # Compute counts - new_file_count = sum(1 for ln in new_hunk_lines if ln.startswith((" ", "+"))) - old_count = sum(1 for ln in new_hunk_lines if ln.startswith((" ", "-"))) - header = f"@@ -{expansion_start},{old_count} +{expansion_start},{new_file_count} @@" - - return header, new_hunk_lines - - -def _rebuild_patch( - raw_patch: str, - merged_hunks: list[dict[str, Any]], - source_lines: list[str], -) -> str | None: - """Rebuild the patch with expanded context. - - Args: - raw_patch: The original patch - merged_hunks: List of merged expanded hunks - source_lines: Source file lines - - Returns: - Rebuilt patch string or None if rebuild fails - """ - if not merged_hunks: - return None - lines = raw_patch.split("\n") - header_lines: list[str] = [] - for line in lines: - if line.startswith("@@"): - break - header_lines.append(line) - result_lines: list[str] = list(header_lines) - for merged_hunk in merged_hunks: - hunk_header, hunk_lines = _build_expanded_hunk(merged_hunk, source_lines) - result_lines.append(hunk_header) - result_lines.extend(hunk_lines) - return "\n".join(result_lines) diff --git a/src/codespy/tools/git/server.py b/src/codespy/tools/git/server.py index 5194c84..24de8ae 100644 --- a/src/codespy/tools/git/server.py +++ b/src/codespy/tools/git/server.py @@ -3,7 +3,7 @@ import logging import os -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.config import Settings from codespy.tools.git.base import GitClient @@ -14,7 +14,7 @@ # Get caller module from environment (set by mcp_utils.py) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("git") +mcp = MCPServer("git") _settings: Settings | None = None @@ -155,10 +155,6 @@ def detect_git_platform(url: str) -> dict: if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - # Initialize with settings from environment _settings = Settings() mcp.run() diff --git a/src/codespy/tools/mcp_utils.py b/src/codespy/tools/mcp_utils.py index e075d4d..93fb2b2 100644 --- a/src/codespy/tools/mcp_utils.py +++ b/src/codespy/tools/mcp_utils.py @@ -7,8 +7,7 @@ from typing import Any import dspy # type: ignore[import-untyped] -from mcp import ClientSession, StdioServerParameters # type: ignore[import-not-found] -from mcp.client.stdio import stdio_client # type: ignore[import-not-found] +from mcp import Client, StdioServerParameters logger = logging.getLogger(__name__) @@ -24,7 +23,7 @@ async def connect_mcp_server( Args: mcp_path: Path to the MCP server Python module args: Additional command-line arguments for the server - contexts: List to append context managers for cleanup (transport, session) + contexts: List to append context managers for cleanup (client) caller_module: Name of the calling module for logging (e.g., 'supply_chain_auditor') Returns: @@ -39,23 +38,18 @@ async def connect_mcp_server( env = os.environ.copy() env["MCP_CALLER_MODULE"] = caller_module - params = StdioServerParameters( - command=sys.executable, - args=[str(mcp_path)] + args, - env=env, + client = Client( + StdioServerParameters( + command=sys.executable, + args=[str(mcp_path)] + args, + env=env, + ) ) + await client.__aenter__() + contexts.append(client) - transport = stdio_client(params) - streams = await transport.__aenter__() - contexts.append(transport) - - session = ClientSession(*streams) - await session.__aenter__() - contexts.append(session) - await session.initialize() - - tools_response = await session.list_tools() - return [dspy.Tool.from_mcp_tool(session, tool) for tool in tools_response.tools] + tools_response = await client.list_tools() + return [dspy.Tool.from_mcp_tool(client, tool) for tool in tools_response.tools] async def cleanup_mcp_contexts(contexts: list[Any]) -> None: diff --git a/src/codespy/tools/parsers/ripgrep/server.py b/src/codespy/tools/parsers/ripgrep/server.py index a7a53ed..1815764 100644 --- a/src/codespy/tools/parsers/ripgrep/server.py +++ b/src/codespy/tools/parsers/ripgrep/server.py @@ -6,14 +6,14 @@ from dataclasses import asdict from functools import lru_cache -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.parsers.ripgrep.client import RipgrepSearch logger = logging.getLogger(__name__) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("ripgrep") +mcp = MCPServer("ripgrep") _search: RipgrepSearch | None = None @@ -184,10 +184,6 @@ def search_literal( if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - repo_path = sys.argv[1] if len(sys.argv) > 1 else "." _search = RipgrepSearch(repo_path) mcp.run() diff --git a/src/codespy/tools/parsers/treesitter/server.py b/src/codespy/tools/parsers/treesitter/server.py index 773c1dc..fd4a6ef 100644 --- a/src/codespy/tools/parsers/treesitter/server.py +++ b/src/codespy/tools/parsers/treesitter/server.py @@ -8,14 +8,14 @@ from pathlib import Path from typing import Any -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.parsers.treesitter.parser import TreeSitterParser logger = logging.getLogger(__name__) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("treesitter") +mcp = MCPServer("treesitter") _parser: TreeSitterParser | None = None @@ -342,10 +342,6 @@ def get_terraform_summary(file_path: str, content: str | None = None) -> dict[st if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - repo_path = sys.argv[1] if len(sys.argv) > 1 else "." _parser = TreeSitterParser(Path(repo_path)) mcp.run() diff --git a/src/codespy/tools/storage/filesystem/server.py b/src/codespy/tools/storage/filesystem/server.py index 295cda4..108b158 100644 --- a/src/codespy/tools/storage/filesystem/server.py +++ b/src/codespy/tools/storage/filesystem/server.py @@ -6,7 +6,7 @@ from collections import OrderedDict from functools import lru_cache -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.storage.filesystem.client import FileSystem @@ -15,7 +15,7 @@ # Get caller module from environment (set by mcp_utils.py) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("filesystem") +mcp = MCPServer("filesystem") _fs: FileSystem | None = None # Manual cache for read_file to skip caching error results @@ -157,10 +157,6 @@ def get_file_info(path: str = "") -> dict: if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - root = sys.argv[1] if len(sys.argv) > 1 else "." _fs = FileSystem(root) mcp.run() diff --git a/src/codespy/tools/storage/s3/server.py b/src/codespy/tools/storage/s3/server.py index dc71e47..c393005 100644 --- a/src/codespy/tools/storage/s3/server.py +++ b/src/codespy/tools/storage/s3/server.py @@ -6,7 +6,7 @@ from collections import OrderedDict from functools import lru_cache -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.storage.s3.client import S3Client @@ -15,7 +15,7 @@ # Get caller module from environment (set by mcp_utils.py) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("s3") +mcp = MCPServer("s3") _client: S3Client | None = None # Manual cache for read_file to skip caching error results @@ -221,10 +221,6 @@ def delete_file(path: str) -> dict: if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - if len(sys.argv) < 2: print("Usage: python server.py [region] [endpoint_url]", file=sys.stderr) sys.exit(1) diff --git a/src/codespy/tools/web/server.py b/src/codespy/tools/web/server.py index 4bee6e6..d572464 100644 --- a/src/codespy/tools/web/server.py +++ b/src/codespy/tools/web/server.py @@ -3,14 +3,14 @@ import logging import os -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.tools.web.client import WebBrowser logger = logging.getLogger(__name__) _caller_module = os.environ.get("MCP_CALLER_MODULE", "unknown") -mcp = FastMCP("web") +mcp = MCPServer("web") _browser: WebBrowser | None = None @@ -69,9 +69,5 @@ def search_and_fetch(query: str, num_results: int = 3) -> list[dict]: if __name__ == "__main__": - # Suppress noisy MCP server "Processing request" logs - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - _browser = WebBrowser() mcp.run() diff --git a/src/codespy/workflows/review/models.py b/src/codespy/workflows/review/models.py index 187b78e..8f4026d 100644 --- a/src/codespy/workflows/review/models.py +++ b/src/codespy/workflows/review/models.py @@ -9,6 +9,29 @@ from codespy.agents.review.models import Issue, IssueCategory, IssueSeverity +# Appended to a memory text cut to fit the review body size limit +TRUNCATED_MARKER = "…(truncated)" + + +class MemorySection(BaseModel): + """One section of a recalled memory (e.g., 'Briefing: ...', 'Around this work').""" + + title: str = Field(description="Section title without markdown heading markers") + text: str = Field(description="Section body text") + + +class RecalledMemory(BaseModel): + """A recalled memory entry from Prefrontal.""" + + task: str = Field(description="Task name that loaded the memory (e.g., 'scope', 'review')") + sections: list[MemorySection] = Field( + default_factory=list, description="Sections of the recalled memory" + ) + nested: bool = Field( + default=True, + description="False = sections render directly under the memories section, without a per-recall block", + ) + class SignatureStatsResult(BaseModel): """Statistics for a single signature's execution during review.""" @@ -20,6 +43,19 @@ class SignatureStatsResult(BaseModel): tokens: int = Field(default=0, description="Tokens used by this signature") call_count: int = Field(default=0, description="Number of LLM calls made by this signature") duration_seconds: float = Field(default=0.0, description="Execution time in seconds") + input_tokens: int = Field(default=0, description="Input/prompt tokens used") + output_tokens: int = Field(default=0, description="Output/completion tokens used") + input_cost: float = Field(default=0.0, description="Cost for input tokens") + output_cost: float = Field(default=0.0, description="Cost for output tokens") + @property + def is_memory(self) -> bool: + """Check if this signature is a memory-related signature. + + Memory signatures have names starting with MEMORY_UNIT_PREFIX ("memory_"). + """ + from codespy.config_memory import MEMORY_UNIT_PREFIX + + return self.name.startswith(MEMORY_UNIT_PREFIX) @property def cost_per_call(self) -> float: @@ -90,6 +126,14 @@ class ReviewResult(BaseModel): signature_stats: list[SignatureStatsResult] = Field( default_factory=list, description="Per-signature statistics (cost, tokens, time)" ) + memories: list[RecalledMemory] = Field( + default_factory=list, + description="Prefrontal pre-call memory injected into agent inputs", + ) + memory_pending: bool = Field( + default=False, + description="True when memory phase (saves, retain, consolidation) is still pending", + ) @property def total_issues(self) -> int: @@ -139,6 +183,9 @@ def to_markdown(self) -> str: "", ] + # Memories section (collapsible) - placed before Summary + lines.extend(self.memories_markdown_lines()) + # Overall summary if self.overall_summary: lines.extend(["## Summary", "", self.overall_summary, ""]) @@ -169,29 +216,15 @@ def to_markdown(self) -> str: "## Cost", "", f"- **LLM Calls:** {self.llm_calls}", - f"- **Total Tokens:** {self.total_tokens:,}", f"- **Total Cost:** ${self.total_cost:.4f}", - "", ] ) + if self.memory_pending: + lines.append("- **Memory:** pending (retain, consolidation, mental models)") + lines.append("") # Per-signature breakdown - if self.signature_stats: - lines.extend( - [ - "### Per-Signature Breakdown", - "", - "| Signature | Cost | Tokens | Calls | Duration |", - "|-----------|------|--------|-------|----------|", - ] - ) - for stats in sorted(self.signature_stats, key=lambda x: x.cost, reverse=True): - duration_str = f"{stats.duration_seconds:.1f}s" - lines.append( - f"| {stats.name} | ${stats.cost:.4f} | {stats.tokens:,} | " - f"{stats.call_count} | {duration_str} |" - ) - lines.append("") + lines.extend(self.cost_breakdown_markdown_lines("###")) # Issues by severity if self.issues: @@ -266,6 +299,188 @@ def to_markdown(self) -> str: return "\n".join(lines) + def cost_breakdown_markdown_lines(self, heading: str = "###") -> list[str]: + """Build cost breakdown tables for Review and Memory groups. + + Each non-empty group emits a table with columns: + Signature | In Tokens | Out Tokens | In Cost | Out Cost | Calls | Duration + + Review signatures come first, then Memory. Each table has a subtotal line. + Groups with no rows are omitted. If signature_stats is empty, returns []. + + Args: + heading: Heading prefix (e.g., "###" or "####") + + Returns: + Markdown lines for the cost breakdown section. + """ + if not self.signature_stats: + return [] + + review_stats = [s for s in self.signature_stats if not s.is_memory] + memory_stats = [s for s in self.signature_stats if s.is_memory] + + def table_lines(label: str, stats: list[SignatureStatsResult]) -> list[str]: + if not stats: + return [] + sorted_stats = sorted(stats, key=lambda x: x.cost, reverse=True) + subtotal_cost = sum(s.cost for s in sorted_stats) + subtotal_calls = sum(s.call_count for s in sorted_stats) + lines = [ + f"{heading} {label}", + "", + "| Signature | In Tokens | Out Tokens | In Cost | Out Cost | Calls | Duration |", + "|-----------|-----------|------------|---------|----------|-------|----------|", + ] + for s in sorted_stats: + duration_str = f"{s.duration_seconds:.1f}s" + lines.append( + f"| {s.name} | {s.input_tokens:,} | {s.output_tokens:,} | " + f"${s.input_cost:.4f} | ${s.output_cost:.4f} | {s.call_count} | {duration_str} |" + ) + lines.extend(["", f"**Subtotal:** ${subtotal_cost:.4f} | **LLM Calls:** {subtotal_calls}", ""]) + return lines + + result: list[str] = [] + result.extend(table_lines("Review", review_stats)) + result.extend(table_lines("Memory", memory_stats)) + return result + + def memories_markdown_lines( + self, summary: str = "memories", max_chars: int | None = None + ) -> list[str]: + """Build the collapsible memories section with nested per-recall and per-section details. + + Structure (flat run-level sections, nested scope recall): +
{summary} +
+ + +
{section_title} +
{body}
+
+ + +
{task} +
+
{section_title} +
{body}
+
+ ... +
+
+ +
+
+ + GitReporter passes ``max_chars`` to stay within GitHub's 65,536 char + review body limit: memory texts are truncated in order, other sections + are never touched. + + Args: + summary: Summary text of the outer collapsible section. + max_chars: Character budget for the whole section (None = unlimited). + + Returns: + Markdown lines, or ``[]`` when there is no memory or nothing fits. + """ + if not self.memories: + return [] + + def size(block_lines: list[str]) -> int: + # Lines are joined with "\n" by the caller + return sum(len(line) + 1 for line in block_lines) + + def recall_head(task: str) -> list[str]: + return ["
", f"{task}", "
", ""] + + def recall_tail() -> list[str]: + return ["
", "
", ""] + + def section_block(title: str, body: str) -> list[str]: + return [ + "
", + f"{title}", + "
", + "", + body, + "", + "
", + "
", + "", + ] + + # Reserve room for outer head/tail and per-recall tails + outer_head = ["
", f"{summary}", "
", ""] + outer_tail = ["
", "
", ""] + outer_overhead = size(outer_head) + size(outer_tail) + + lines: list[str] = [] + remaining = None if max_chars is None else max_chars - outer_overhead + outer_stopped = False + + for memory in self.memories: + if outer_stopped: + break + + r_head = recall_head(memory.task) + r_tail = recall_tail() + recall_overhead = size(r_head) + size(r_tail) + + # For flat (nested=False), no recall head/tail overhead + if memory.nested: + # Budget check: need at least recall overhead + one section + if remaining is not None and remaining < recall_overhead: + break + + if remaining is not None: + remaining -= recall_overhead + else: + # Flat: no recall overhead, sections go directly into lines + recall_overhead = 0 + + r_lines: list[str] = list(r_head) if memory.nested else [] + has_section = False + recall_stopped = False + + for section in memory.sections: + if recall_stopped: + break + + s_block = section_block(section.title, section.text) + s_size = size(s_block) + if remaining is None or s_size <= remaining: + r_lines.extend(s_block) + has_section = True + if remaining is not None: + remaining -= s_size + else: + # Try to truncate the section body + overhead = size(section_block(section.title, "")) + len(TRUNCATED_MARKER) + if remaining is not None and remaining > overhead: + truncated_body = section.text[: remaining - overhead] + TRUNCATED_MARKER + r_lines.extend(section_block(section.title, truncated_body)) + has_section = True + # Account for the truncated section size + remaining -= overhead + len(truncated_body) + # Close this recall - stop processing sections for this recall + recall_stopped = True + # Also stop the outer loop - no more recalls fit + outer_stopped = True + + if memory.nested: + r_lines.extend(r_tail) + + if has_section: + lines.extend(r_lines) + elif memory.nested and remaining is not None: + # No section rendered for this recall: give back its overhead + remaining += recall_overhead + + if not lines: + return [] + return outer_head + lines + outer_tail + def to_json_dict(self) -> dict: """Convert to a JSON-serializable dictionary.""" return self.model_dump(mode="json") diff --git a/src/codespy/workflows/review/pipeline.py b/src/codespy/workflows/review/pipeline.py index a3f2304..3983cce 100644 --- a/src/codespy/workflows/review/pipeline.py +++ b/src/codespy/workflows/review/pipeline.py @@ -4,12 +4,15 @@ import logging import uuid from pathlib import Path +from typing import Any import dspy # type: ignore[import-untyped] from codespy.agents import configure_dspy, get_cost_tracker, verify_model_access from codespy.agents.memory.hippocampus.context_memory import Topic +from codespy.agents.memory.prefrontal import build_facets +from codespy.agents.memory.prefrontal.query import MAX_CONTEXT_PATHS, MAX_ENTITY_TERMS from codespy.agents.review.models import Issue, PRContext, ReviewContext, ReviewMetadata from codespy.agents.review import ( Auditor, @@ -19,16 +22,27 @@ Summarizer, SupplyChainAuditor, ) -from codespy.agents.memory.hippocampus.episode import join_episode_saves +from codespy.agents.memory.hippocampus.episode import ( + defer_episode_saves, + join_episode_saves, + start_deferred_episode_saves, +) from codespy.agents.review.helpers import build_patches -from codespy.agents.review.scope import MANIFEST_FILES, MANIFEST_GLOBS +from codespy.agents.review.scope import MANIFEST_FILES, MANIFEST_GLOBS, build_sparse_patterns from codespy.config import Settings, get_settings -from codespy.config_memory import verify_memory_access +from codespy.config_memory import ( + get_cerebral, + get_episode_store, + get_run_prefrontal, + verify_memory_access, +) from codespy.tools.git import ChangedFile, GitClient, PullRequest, get_client from codespy.tools.git.local_diff import build_pr_from_diff -from codespy.tools.git.patch_utils import compact_patches + from codespy.workflows.review.models import ( LocalReviewConfig, + MemorySection, + RecalledMemory, RemoteReviewConfig, ReviewConfig, ReviewResult, @@ -89,7 +103,7 @@ def _fetch_pr(self, pr_url: str) -> PullRequest: def _get_repo_path(self, pr: PullRequest) -> Path: """Get the local repository path for a MR, creating directories if needed.""" - cache_dir = self.settings.cache_dir + cache_dir = self.settings.review.cache_dir cache_dir.mkdir(parents=True, exist_ok=True) # Handle nested namespaces for GitLab owner_path = pr.repo_owner.replace("/", "_") @@ -148,13 +162,16 @@ def _build_local_pr(self, config: LocalReviewConfig) -> PullRequest: ) def forward(self, config: ReviewConfig) -> ReviewResult: - """Run the complete review pipeline. + """Run the complete review pipeline (review phase only). + + The review phase includes scope identification, summarizer, review modules, + and audit. Episode saves are deferred until finish_memory() is called. Args: config: Review configuration (RemoteReviewConfig or LocalReviewConfig) Returns: - ReviewResult with issues, summary, costs, etc. + ReviewResult with issues, summary, costs, etc. (memory_pending=True) """ self.cost_tracker.reset() @@ -183,8 +200,37 @@ def forward(self, config: ReviewConfig) -> ReviewResult: else: raise ValueError(f"Invalid config type: {type(config)}") - # Step 1: Identify scopes FIRST + # Run the review phase with deferred episode saves + try: + with defer_episode_saves(): + result = self._run_review_phase(config, pr, repo_path, run_id) + return result + except Exception: + # Ensure saves are started on exception before re-raising + start_deferred_episode_saves() + raise + + def _run_review_phase( + self, + config: ReviewConfig, + pr: Any, + repo_path: Path, + run_id: str, + ) -> ReviewResult: + """Run the review phase (scope identification through audit). + + Args: + config: Review configuration + pr: PullRequest object + repo_path: Path to the repository + run_id: Unique run identifier + + Returns: + ReviewResult with memory_pending=True + """ is_local = isinstance(config, LocalReviewConfig) + + # Step 1: Identify scopes FIRST logger.info("Identifying code scopes...") pr_context = PRContext( repo_slug=pr.repo_slug, @@ -209,20 +255,54 @@ def forward(self, config: ReviewConfig) -> ReviewResult: logger.info(f" Lock file: {manifest.lock_file_path}") if manifest.dependencies_changed: logger.info(" Dependencies changed: Yes") + # Expand sparse checkout to cover full scope subtrees - if not is_local: - self._expand_sparse_for_scopes(scopes, repo_path) changed_file_paths = [f.filename for f in pr.changed_files] + if not is_local: + self._expand_sparse_for_scopes(scopes, repo_path, changed_file_paths) patches = build_patches(pr.changed_files) - 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) + # 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()) + + # Build scope topic IDs for Prefrontal + scope_topic_ids = [t.id for t in all_scope_topics if t.type == "project_scope"] + + # Run-level Prefrontal recall: one load shared by all consumer agents + run_pf = get_run_prefrontal(self.settings, pr.repo_full_name, scope_topic_ids) + pf_text = "" + if run_pf: + # Build facets for the run-level load + all_paths = list({f.filename for s in scopes for f in s.changed_files}) + all_packages = list({ + s.package_manifest.package_name + for s in scopes + if s.package_manifest and s.package_manifest.package_name + }) + facets = build_facets( + task="review", + target=pr.repo_full_name, + pr_title=pr.title, + summary=pr.body or "", + paths=all_paths[:MAX_CONTEXT_PATHS], + packages=all_packages[:MAX_ENTITY_TERMS], + ) + pf_text = run_pf.load(facets) + # Persist run-level recalls + try: + store = get_episode_store(self.settings) + if store and run_pf.recalls: + store.save_recalls( + run_pf.recalls, + run_id=run_id, + episode_id=None, + task="review", + ) + except Exception as e: + logger.warning(f"Failed to save run-level recalls: {e}") + + # Step 2: Run Summarizer pr_summary = self.summarizer( pr_context=pr_context, changed_file_paths=changed_file_paths, @@ -230,10 +310,14 @@ def forward(self, config: ReviewConfig) -> ReviewResult: run_id=run_id, scopes=scopes, topics=all_scope_topics, + prefrontal_memory=pf_text, ) - # Enrich review_ctx with actual summary + # Enrich review_ctx with actual summary and prefrontal_memory pr_context.summary = pr_summary - review_ctx = ReviewContext(pr_context=pr_context, memory=None, metadata=metadata) + review_ctx = ReviewContext( + pr_context=pr_context, memory=None, metadata=metadata, prefrontal_memory=pf_text + ) + # Step 3: Run review modules concurrently via asyncio.gather module_names = ["code_reviewer", "doc_reviewer", "supply_chain_auditor"] logger.info(f"Running review modules concurrently: {', '.join(module_names)}...") @@ -241,7 +325,8 @@ def forward(self, config: ReviewConfig) -> ReviewResult: self._run_review_modules(scopes, module_names, review_context=review_ctx) ) logger.info(f"Found {len(all_issues)} issues") - # Step 4: Run Audit (loads own prior episodes per scope, no memory inheritance from parallel modules) + + # Step 4: Run Audit quality_assessment, recommendation = self.auditor( review_context=review_ctx, all_issues=all_issues, @@ -249,18 +334,37 @@ def forward(self, config: ReviewConfig) -> ReviewResult: scopes=scopes, topics=all_scope_topics, ) - # Ensure all background episode saves complete before stats collection - # (audit's episode save runs in background and contains distiller/cartographer calls) - join_episode_saves() - # Collect per-signature statistics (after all saves complete) + + # Build memories list from scope and run-level Prefrontal loads + # Run-level first (flat), then scope (nested) + memories: list[RecalledMemory] = [] + if run_pf and run_pf.last_sections: + memories.append( + RecalledMemory( + task="review", + nested=False, + sections=[MemorySection(title=t, text=b) for t, b in run_pf.last_sections], + ) + ) + scope_pf_sections = self.scope_resolver.prefrontal_sections + if scope_pf_sections: + memories.append( + RecalledMemory( + task="scope", + sections=[MemorySection(title=t, text=b) for t, b in scope_pf_sections], + ) + ) + + # Collect stats at end of review phase (before memory phase) signature_stats_list = self._collect_signature_stats() + return ReviewResult( pr_number=pr.number, pr_title=pr.title, pr_url=pr.url, repo=pr.repo_full_name, run_id=run_id, - model_used=self.settings.default_model, + model_used=self.settings.llm.default_model, issues=all_issues, overall_summary=pr_summary, quality_assessment=quality_assessment, @@ -269,8 +373,69 @@ def forward(self, config: ReviewConfig) -> ReviewResult: total_tokens=self.cost_tracker.total_tokens, llm_calls=self.cost_tracker.call_count, signature_stats=signature_stats_list, + memories=memories, + memory_pending=True, ) + def finish_memory(self, result: ReviewResult) -> ReviewResult: + """Run the memory phase: start deferred saves, join them, consolidate. + + Args: + result: ReviewResult from forward() with memory_pending=True + + Returns: + ReviewResult with updated costs and memory_pending=False. + Never raises - errors are logged and the result is returned. + """ + if not result.memory_pending: + return result + + try: + # Start deferred saves (they were queued during review phase) + start_deferred_episode_saves() + # Wait for all saves to complete + join_episode_saves() + # Run consolidation and mental model update + self._consolidate_run(result.run_id) + # Recollect stats with memory costs + signature_stats_list = self._collect_signature_stats() + + return result.model_copy(update={ + "signature_stats": signature_stats_list, + "total_cost": self.cost_tracker.total_cost, + "total_tokens": self.cost_tracker.total_tokens, + "llm_calls": self.cost_tracker.call_count, + "memory_pending": False, + }) + except Exception as e: + logger.warning("Memory phase failed: %s", e, exc_info=True) + # Return result as-is, but mark memory as no longer pending + # (we tried and failed, don't keep showing "pending") + return result.model_copy(update={"memory_pending": False}) + + def _consolidate_run(self, run_id: str) -> None: + """Trigger one consolidation per run after all episode saves complete. + + Consolidation runs only when: + - Memory is enabled globally + - Cerebral is available + - At least one signature is enabled + """ + from codespy.config_dspy import SIGNATURE_NAMES + + # Check if memory is enabled and at least one signature is enabled + if not self.settings.memory.enabled or not any( + self.settings.is_signature_enabled(sig) for sig in SIGNATURE_NAMES + ): + return + + try: + cerebral = get_cerebral(self.settings) + if cerebral: + cerebral.consolidate_run(run_id) + except Exception: + logger.warning("Consolidation failed for run %s", run_id, exc_info=True) + def _collect_signature_stats(self) -> list[SignatureStatsResult]: """Collect statistics from all signatures that executed. @@ -288,16 +453,29 @@ def _collect_signature_stats(self) -> list[SignatureStatsResult]: tokens=stats.tokens, call_count=stats.call_count, duration_seconds=stats.duration_seconds, + input_tokens=stats.input_tokens, + output_tokens=stats.output_tokens, + input_cost=stats.input_cost, + output_cost=stats.output_cost, ) ) return stats_list - def _expand_sparse_for_scopes(self, scopes: list, repo_path: Path) -> None: + def _expand_sparse_for_scopes(self, scopes: list, repo_path: Path, changed_files: list[str] | None = None) -> None: """Expand sparse checkout to cover full subtree of each identified scope. - Called after scope identification, before compact_patches and review modules, + Called after scope identification and before review modules, to ensure read_file and patch compaction have full scope context available. + + Uses the same sparse pattern builder as derive_sparse_paths to ensure + manifests and AI instruction files are consistently anchored to ancestor + dirs of changed files, avoiding repo-wide pattern matching. + + Args: + scopes: List of identified scope results + repo_path: Path to the repository root + changed_files: Optional list of changed file paths (if None, extracts from scopes) """ from git import Repo from git.exc import GitCommandError @@ -306,26 +484,41 @@ def _expand_sparse_for_scopes(self, scopes: list, repo_path: Path) -> None: if not git_dir.exists(): return - # Build scope-aware sparse paths - sparse_paths: set[str] = set() + # Check for root scope — disable sparse checkout entirely + has_root_scope = any(s.subroot == "." for s in scopes) + if has_root_scope: + try: + repo = Repo(repo_path) + repo.git.update_environment(GIT_TERMINAL_PROMPT="0") + # Disable sparse checkout to get full repo + repo.git.config("core.sparseCheckout", "false") + # Re-checkout to materialize everything + if repo.head.is_valid(): + repo.git.checkout() + logger.info("Root scope: disabled sparse checkout, full repo checked out") + return + except (GitCommandError, ValueError, TypeError) as e: + logger.warning("Failed to disable sparse checkout for root scope: %s", e) + return + + # Build sparse patterns using the canonical builder + if changed_files is None: + changed_files = [] + for scope in scopes: + changed_files.extend(f.filename for f in scope.changed_files) + + sparse_paths = build_sparse_patterns(changed_files) + + # Add scope subtrees (full directories) for expanded scope context for scope in scopes: - if scope.subroot == ".": - # Root scope — need everything; disable sparse checkout effectively - sparse_paths.add("/*") - sparse_paths.add("*/") - break - else: - sparse_paths.add(scope.subroot.rstrip("/") + "/") - - # Always include root-level files and manifests - sparse_paths.add("/*") - for manifest in MANIFEST_FILES: - sparse_paths.add(manifest) - for pattern in MANIFEST_GLOBS: - sparse_paths.add(pattern) + if scope.subroot != ".": + sparse_paths.append(f"/{scope.subroot.rstrip('/')}/") + + # Deduplicate and sort + sparse_paths = sorted(set(sparse_paths)) sparse_file = git_dir / "info" / "sparse-checkout" - sparse_file.write_text("\n".join(sorted(sparse_paths)) + "\n") + sparse_file.write_text("\n".join(sparse_paths) + "\n") # Re-checkout to materialize newly included paths try: @@ -338,5 +531,6 @@ def _expand_sparse_for_scopes(self, scopes: list, repo_path: Path) -> None: ) return repo.git.checkout() + logger.debug("Sparse checkout expanded for %d scope(s)", len(scopes)) except (GitCommandError, ValueError, TypeError) as e: logger.warning("Sparse checkout expansion failed (non-fatal): %s", e) diff --git a/src/codespy/workflows/review/reporters/git.py b/src/codespy/workflows/review/reporters/git.py index bda5a26..fe83959 100644 --- a/src/codespy/workflows/review/reporters/git.py +++ b/src/codespy/workflows/review/reporters/git.py @@ -1,11 +1,15 @@ """Git reporter for posting review comments to GitHub/GitLab.""" import logging +import time from typing import TYPE_CHECKING -from codespy.agents.review.models import Issue, IssueSeverity +from collections import Counter + +from codespy.agents.review.models import Issue, IssueCategory, IssueSeverity from codespy.workflows.review.models import ReviewResult from codespy.workflows.review.reporters.base import BaseReporter +from codespy.tools.git.base import SubmittedReview from codespy.tools.git.client import get_client if TYPE_CHECKING: @@ -25,6 +29,12 @@ class GitReporter(BaseReporter): IssueSeverity.INFO: "⚪", } + # Review body budget, below GitHub's 65,536 char limit + MAX_BODY_CHARS = 60000 + + # Retry configuration for update + UPDATE_RETRY_DELAYS = [2.0, 4.0, 8.0] # seconds + def __init__( self, url: str, @@ -40,11 +50,25 @@ def __init__( self.client = get_client(url, settings) def report(self, result: ReviewResult) -> None: - """Post review result to the merge request. + """Post review result to the merge request (backward compatibility). Args: result: The review result to post. """ + self.publish(result) + + def publish(self, result: ReviewResult) -> SubmittedReview | None: + """Publish the review to the merge request. + + Posts the initial review after the audit phase (before memory phase). + The review will show "Memory: pending" in costs if memory_pending is True. + + Args: + result: The review result to publish. + + Returns: + SubmittedReview handle for later update, or None if publish failed. + """ # Separate issues with and without line numbers inline_issues: list[Issue] = [] body_issues: list[Issue] = [] @@ -62,27 +86,81 @@ def report(self, result: ReviewResult) -> None: comments = self._build_inline_comments(inline_issues) # Submit the review - self.client.submit_review( + handle = self.client.submit_review( url=self.url, body=body, comments=comments, ) - logger.info( - f"Posted {self.client.platform_name} review with {len(comments)} inline comments " - f"and {len(body_issues)} issues in body" + if handle: + logger.info( + f"Posted {self.client.platform_name} review with {len(comments)} inline comments " + f"and {len(body_issues)} issues in body" + ) + return handle + + def update(self, handle: SubmittedReview, result: ReviewResult) -> None: + """Update an existing review with new costs after memory phase. + + Rebuilds the body with full costs and edits the review. + Retries up to 3 times with exponential backoff on failure. + + Args: + handle: The SubmittedReview returned by publish(). + result: The updated review result with full costs. + """ + # Separate issues with and without line numbers + inline_issues: list[Issue] = [] + body_issues: list[Issue] = [] + + for issue in result.issues: + if issue.line_start is not None: + inline_issues.append(issue) + else: + body_issues.append(issue) + + # Build review body, reserving space for the body suffix + body = self._build_review_body(result, body_issues, reserve=len(handle.body_suffix)) + + # Append the body suffix (fallback comments from original submission) + if handle.body_suffix: + body = body + handle.body_suffix + + # Try to update with retries + last_error: Exception | None = None + for attempt, delay in enumerate(self.UPDATE_RETRY_DELAYS): + try: + self.client.update_review(self.url, handle, body) + logger.info(f"Updated {self.client.platform_name} review with full costs") + return + except Exception as e: + last_error = e + logger.warning( + f"Update attempt {attempt + 1} failed: {e}. Retrying in {delay}s..." + ) + time.sleep(delay) + + # All retries exhausted + logger.warning( + f"Failed to update {self.client.platform_name} review after " + f"{len(self.UPDATE_RETRY_DELAYS)} attempts. Keeping original post." ) + # Log the actual error at debug level + if last_error: + logger.debug("Final update error", exc_info=last_error) def _build_review_body( self, result: ReviewResult, body_issues: list[Issue], + reserve: int = 0, ) -> str: """Build the review body with collapsible sections. Args: result: The review result. body_issues: Issues without line numbers to include in body. + reserve: Number of characters to reserve for body suffix (e.g., appended comments). Returns: Formatted markdown string for review body. @@ -100,6 +178,9 @@ def _build_review_body( ) lines.append("") + # Record insertion point for memories section (after header) + insert_at = len(lines) + # Summary section if result.overall_summary: lines.extend( @@ -128,28 +209,8 @@ def _build_review_body( ] ) - # Statistics section - lines.extend( - [ - "
", - "📊 Statistics", - "", - "| Metric | Count |", - "|--------|-------|", - f"| Total Issues | {result.total_issues} |", - f"| Critical | {len(result.critical_issues)} |", - f"| High | {sum(1 for i in result.issues if i.severity == IssueSeverity.HIGH)} |", - f"| Medium | {sum(1 for i in result.issues if i.severity == IssueSeverity.MEDIUM)} |", # noqa: E501 - f"| Low | {sum(1 for i in result.issues if i.severity == IssueSeverity.LOW)} |", - f"| Security | {len(result.security_issues)} |", - f"| Bugs | {len(result.bug_issues)} |", - f"| Documentation | {len(result.documentation_issues)} |", - f"| Info | {sum(1 for i in result.issues if i.severity == IssueSeverity.INFO)} |", # noqa: E501 - "", - "
", - "", - ] - ) + # Statistics section (severity × category matrix) + lines.extend(self._build_statistics_section(result)) # Cost section if result.total_cost > 0 or result.llm_calls > 0: @@ -159,26 +220,14 @@ def _build_review_body( "💰 Cost Summary", "", f"**Total:** ${result.total_cost:.4f} | " - f"**Tokens:** {result.total_tokens:,} | " f"**LLM Calls:** {result.llm_calls}", - "", ] ) + if result.memory_pending: + lines.append("- **Memory:** pending (retain, consolidation, mental models)") + lines.append("") - if result.signature_stats: - lines.extend( - [ - "| Signature | Cost | Tokens | Calls | Duration |", - "|-----------|------|--------|-------|----------|", - ] - ) - for stats in sorted(result.signature_stats, key=lambda x: x.cost, reverse=True): - duration_str = f"{stats.duration_seconds:.1f}s" - lines.append( - f"| {stats.name} | ${stats.cost:.4f} | {stats.tokens:,} | " - f"{stats.call_count} | {duration_str} |" - ) - lines.append("") + lines.extend(result.cost_breakdown_markdown_lines("####")) lines.extend( [ @@ -252,8 +301,87 @@ def _build_review_body( ] ) + # Memories section (collapsible, size-limited for GitHub's 65,536 char body limit) + # Insert after header, before Summary + if result.memories: + # Calculate budget after building all other sections, minus reserve + other_sections_len = len("\n".join(lines)) + 1 # +1 for trailing newline + budget = self.MAX_BODY_CHARS - reserve - other_sections_len - 1 + memory_lines = result.memories_markdown_lines( + summary="🧠 memories", max_chars=max(0, budget) + ) + if memory_lines: + lines[insert_at:insert_at] = memory_lines + return "\n".join(lines) + def _build_statistics_section(self, result: ReviewResult) -> list[str]: + """Build the statistics section as a severity × category matrix. + + Args: + result: The review result containing issues. + + Returns: + Markdown lines for the statistics section. + """ + severities = [ + IssueSeverity.CRITICAL, + IssueSeverity.HIGH, + IssueSeverity.MEDIUM, + IssueSeverity.LOW, + IssueSeverity.INFO, + ] + categories = [ + (IssueCategory.SECURITY, "Security"), + (IssueCategory.BUG, "Bugs"), + (IssueCategory.DOCUMENTATION, "Documentation"), + (IssueCategory.SMELL, "Smells"), + ] + + # Build counts with one pass over issues + counts = Counter((issue.severity, issue.category) for issue in result.issues) + + # Calculate row totals (per severity) + row_totals = {s: sum(counts[(s, c)] for c, _ in categories) for s in severities} + + # Calculate column totals (per category) + col_totals = {c: sum(counts[(s, c)] for s in severities) for c, _ in categories} + + # Grand total + grand_total = result.total_issues + + # Build table lines + lines = [ + "
", + "📊 Statistics", + "", + "| Severity | Security | Bugs | Documentation | Smells | Total |", + "|----------|----------|------|---------------|--------|-------|", + ] + + # Data rows + for severity in severities: + cells = [severity.value.title()] + for cat, _ in categories: + cells.append(str(counts[(severity, cat)])) + cells.append(str(row_totals[severity])) + lines.append("| " + " | ".join(cells) + " |") + + # Totals row (bold) + total_row = ["**Total**"] + for cat, _ in categories: + total_row.append(f"**{col_totals[cat]}**") + total_row.append(f"**{grand_total}**") + lines.append("| " + " | ".join(total_row) + " |") + + lines.extend([ + "", + "
", + "", + ]) + + return lines + def _build_inline_comments(self, issues: list[Issue]) -> list[dict]: """Build inline comment dictionaries for the Git API. diff --git a/src/codespy/workflows/review/server.py b/src/codespy/workflows/review/server.py index 035ff0c..33decc8 100644 --- a/src/codespy/workflows/review/server.py +++ b/src/codespy/workflows/review/server.py @@ -7,18 +7,19 @@ from pathlib import Path from typing import Any -from mcp.server.fastmcp import FastMCP +from mcp.server import MCPServer from codespy.config import Settings logger = logging.getLogger(__name__) -mcp = FastMCP("codespy-reviewer") +mcp = MCPServer("codespy-reviewer") _settings: Settings | None = None _pipeline: Any = None # Thread pool for running the review pipeline (which uses asyncio.run() internally) # without conflicting with the MCP server's own event loop. +# Single worker ensures memory phase of one review completes before the next starts. _executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) @@ -33,18 +34,20 @@ def _get_pipeline() -> Any: def _run_in_thread(fn: Any, *args: Any, **kwargs: Any) -> Any: - """Run a function in the thread pool and return the result. + """Run a function in the thread pool and return an asyncio future. The ReviewPipeline uses asyncio.run() internally, which can't be called from within the MCP server's event loop. Running in a separate thread gives the pipeline its own event loop. """ loop = asyncio.get_event_loop() - return loop.run_in_executor(_executor, lambda: fn(*args, **kwargs)) + # Submit to executor and wrap as asyncio future + executor_fut = _executor.submit(fn, *args, **kwargs) + return asyncio.wrap_future(executor_fut) def _do_local_review(repo_path: str, base_ref: str, output_format: str) -> str: - """Synchronous local review — runs in thread pool.""" + """Synchronous local review — runs in thread pool with full memory phase.""" import json from codespy.workflows.review.models import LocalReviewConfig @@ -53,7 +56,8 @@ def _do_local_review(repo_path: str, base_ref: str, output_format: str) -> str: config = LocalReviewConfig(repo_path=repo, base_ref=base_ref, uncommitted=False) pipeline = _get_pipeline() - result = pipeline(config) + # Run review phase then memory phase + result = pipeline.finish_memory(pipeline(config)) if output_format == "json": return json.dumps(result.to_json_dict(), indent=2) @@ -61,7 +65,7 @@ def _do_local_review(repo_path: str, base_ref: str, output_format: str) -> str: def _do_uncommitted_review(repo_path: str, output_format: str) -> str: - """Synchronous uncommitted review — runs in thread pool.""" + """Synchronous uncommitted review — runs in thread pool with full memory phase.""" import json from codespy.workflows.review.models import LocalReviewConfig @@ -70,7 +74,8 @@ def _do_uncommitted_review(repo_path: str, output_format: str) -> str: config = LocalReviewConfig(repo_path=repo, uncommitted=True) pipeline = _get_pipeline() - result = pipeline(config) + # Run review phase then memory phase + result = pipeline.finish_memory(pipeline(config)) if output_format == "json": return json.dumps(result.to_json_dict(), indent=2) @@ -78,7 +83,7 @@ def _do_uncommitted_review(repo_path: str, output_format: str) -> str: def _do_pr_review(pr_url: str, output_format: str) -> str: - """Synchronous PR review — runs in thread pool.""" + """Synchronous PR review — runs in thread pool with full memory phase.""" import json from codespy.workflows.review.models import RemoteReviewConfig @@ -86,7 +91,8 @@ def _do_pr_review(pr_url: str, output_format: str) -> str: config = RemoteReviewConfig(url=pr_url) pipeline = _get_pipeline() - result = pipeline(config) + # Run review phase then memory phase + result = pipeline.finish_memory(pipeline(config)) if output_format == "json": return json.dumps(result.to_json_dict(), indent=2) @@ -184,10 +190,6 @@ def run_server(settings: Settings | None = None) -> None: """Start the MCP server (called from CLI or __main__).""" global _settings - # Suppress noisy MCP server logs — keep the transport clean - logging.getLogger("mcp.server").setLevel(logging.WARNING) - logging.getLogger("mcp.server.lowlevel").setLevel(logging.WARNING) - # Send application logs to stderr (stdout is the MCP transport) logging.basicConfig( level=logging.INFO, @@ -196,7 +198,7 @@ def run_server(settings: Settings | None = None) -> None: ) _settings = settings if settings is not None else Settings() - logger.info(f"codespy-reviewer MCP server starting (model: {_settings.default_model})") + logger.info(f"codespy-reviewer MCP server starting (model: {_settings.llm.default_model})") mcp.run() diff --git a/tests/test_cerebral.py b/tests/test_cerebral.py new file mode 100644 index 0000000..bd32586 --- /dev/null +++ b/tests/test_cerebral.py @@ -0,0 +1,1655 @@ +"""Tests for the Cerebral Hindsight semantic memory module.""" + +from __future__ import annotations + +import os +import uuid +from datetime import UTC, datetime +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from codespy.agents.memory.cerebral import Cerebral +from codespy.agents.memory.cerebral.cost import MeteredLiteLLMSDKEmbeddings +from codespy.agents.memory.hippocampus.context_memory import ( + ContextMemory, + Mutation, + Observation, + OpType, + Topic, +) +from codespy.agents.memory.hippocampus.episode import Episode + + +def async_mock(*args, **kwargs): + """Create a coroutine mock.""" + async def mock_coro(*args, **kwargs): + return None + return mock_coro() + + +@pytest.fixture +def mock_engine(): + """Create a mock MemoryEngine.""" + engine = MagicMock() + # Mock async methods - need to return coroutines + async def mock_coro(*args, **kwargs): + return None + + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_coro + engine.retain_batch_async = mock_coro + return engine + + +@pytest.fixture +def mock_request_context(): + """Create a mock RequestContext.""" + with patch("codespy.agents.memory.cerebral.cerebral.RequestContext") as mock_ctx: + mock_ctx.return_value = MagicMock() + yield mock_ctx + + +@pytest.fixture +def mock_memory_engine_class(mock_engine): + """Create a mock MemoryEngine class.""" + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine") as mock_cls: + mock_cls.return_value = mock_engine + yield mock_cls + + +@pytest.fixture +def mock_litellm(): + """Mock litellm.embedding to avoid API calls during tests.""" + with patch("litellm.embedding") as mock_emb: + mock_emb.return_value = {"data": [{"embedding": [0.1, 0.2, 0.3]}]} + yield mock_emb + + +@pytest.fixture +def episode(): + """Create a test episode with observations and artifacts.""" + topics = [ + Topic(id="test/repo/package", type="project_scope", description="Test package"), + Topic(id="https://github.com/test/repo/pull/1", type="pull_request", description="Test PR"), + ] + + context_memory = ContextMemory( + topics=topics, + context_understanding=[ + Observation( + id="cu-test-1", + content="This is a test observation", + topic_ids=["test/repo/package"], + ), + ], + actions=[ + Observation( + id="ac-test-1", + content="Performed tool call", + topic_ids=["test/repo/package"], + ), + ], + ) + + return Episode( + id=uuid.uuid4(), + run_id="test-run-123", + timestamp=datetime.now(UTC), + task="code_review", + module="CodeReviewer", + question="Review PR #1", + artifacts={"review": "# Review Results\n\nFound 2 issues."}, + context_memory=context_memory, + mutations=[ + Mutation( + step=0, + type=OpType.ADD, + observation_id="cu-test-1", + section="context_understanding", + content="This is a test observation", + previous_content=None, + topic_ids=["test/repo/package"], + ), + Mutation( + step=0, + type=OpType.ADD, + observation_id="ac-test-1", + section="actions", + content="Performed tool call", + previous_content=None, + topic_ids=["test/repo/package"], + ), + ], + ) + + +class TestCerebralInit: + """Tests for Cerebral initialization.""" + + def test_init_creates_engine_with_correct_params(self, mock_memory_engine_class, mock_litellm): + """Test that MemoryEngine is created with correct parameters.""" + Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + llm_model="openai/gpt-4", + llm_api_key="sk-test", + llm_base_url="https://custom.openai.com", + bank_id="test-bank", + ) + + mock_memory_engine_class.assert_called_once_with( + db_url="postgresql://localhost:5432/test", + memory_llm_provider="litellm", + memory_llm_model="openai/gpt-4", + memory_llm_api_key="sk-test", + memory_llm_base_url="https://custom.openai.com", + embeddings=mock_memory_engine_class.call_args.kwargs["embeddings"], + cross_encoder=mock_memory_engine_class.call_args.kwargs["cross_encoder"], + tenant_extension=mock_memory_engine_class.call_args.kwargs["tenant_extension"], + skip_llm_verification=mock_memory_engine_class.call_args.kwargs["skip_llm_verification"], + task_backend=mock_memory_engine_class.call_args.kwargs["task_backend"], + ) + + def test_init_uses_metered_embeddings(self, mock_memory_engine_class, mock_litellm): + """Test that MemoryEngine is created with MeteredLiteLLMSDKEmbeddings.""" + Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + llm_model="openai/gpt-4", + embeddings_model="openai/text-embedding-3-small", + ) + + # Check that embeddings is a MeteredLiteLLMSDKEmbeddings + embeddings = mock_memory_engine_class.call_args.kwargs["embeddings"] + assert isinstance(embeddings, MeteredLiteLLMSDKEmbeddings) + + def test_init_calls_initialize_and_routine_repair(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that initialize() and routine repair are called during construction.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + # initialize(), ensure_maintenance_routines(), repair_orphaned_operations() + assert mock_run_async.call_count == 3 + + def test_init_handles_none_base_url(self, mock_memory_engine_class, mock_litellm): + """Test that None base_url is handled correctly.""" + Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + llm_model="openai/gpt-4", + llm_api_key="sk-test", + llm_base_url=None, + ) + + mock_memory_engine_class.assert_called_once_with( + db_url="postgresql://localhost:5432/test", + memory_llm_provider="litellm", + memory_llm_model="openai/gpt-4", + memory_llm_api_key="sk-test", + memory_llm_base_url=None, + embeddings=mock_memory_engine_class.call_args.kwargs["embeddings"], + cross_encoder=mock_memory_engine_class.call_args.kwargs["cross_encoder"], + tenant_extension=mock_memory_engine_class.call_args.kwargs["tenant_extension"], + skip_llm_verification=mock_memory_engine_class.call_args.kwargs["skip_llm_verification"], + task_backend=mock_memory_engine_class.call_args.kwargs["task_backend"], + ) + + +class TestCerebralBank: + """Tests for Cerebral bank management.""" + + def test_ensure_bank_mission_contains_retracted_invalidates(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that retain_mission contains 'Only RETRACTED invalidates a fact'.""" + captured_mission = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_mission + captured_mission = updates.get("retain_mission") + return None + + async def mock_coro(*args, **kwargs): + return None + + def mock_memory_engine(*args, **kwargs): + engine = MagicMock() + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_update_bank_config + engine.close = mock_coro + return engine + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", mock_memory_engine): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + cerebral._bank_ensured = False + cerebral._ensure_bank() + + assert captured_mission is not None + assert "Only RETRACTED invalidates a fact" in captured_mission + + def test_ensure_bank_mission_is_domain_agnostic(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that retain_mission does not contain code-review-specific wording.""" + captured_mission = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_mission + captured_mission = updates.get("retain_mission") + return None + + async def mock_coro(*args, **kwargs): + return None + + def mock_memory_engine(*args, **kwargs): + engine = MagicMock() + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_update_bank_config + engine.close = mock_coro + return engine + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", mock_memory_engine): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + cerebral._bank_ensured = False + cerebral._ensure_bank() + + assert captured_mission is not None + assert "code review" not in captured_mission.lower() + assert "architectural decisions" not in captured_mission.lower() + assert "dependency relationships" not in captured_mission.lower() + assert "repositories" not in captured_mission.lower() + + def test_ensure_bank_chunk_size_default(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank sends retain_chunk_size == 12288 by default.""" + captured_chunk_size = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_chunk_size + captured_chunk_size = updates.get("retain_chunk_size") + return None + + async def mock_coro(*args, **kwargs): + return None + + def mock_memory_engine(*args, **kwargs): + engine = MagicMock() + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_update_bank_config + engine.close = mock_coro + return engine + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", mock_memory_engine): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + cerebral._bank_ensured = False + cerebral._ensure_bank() + + assert captured_chunk_size == 12288 + + def test_ensure_bank_chunk_size_custom(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank sends custom retain_chunk_size when provided.""" + captured_chunk_size = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_chunk_size + captured_chunk_size = updates.get("retain_chunk_size") + return None + + async def mock_coro(*args, **kwargs): + return None + + def mock_memory_engine(*args, **kwargs): + engine = MagicMock() + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_update_bank_config + engine.close = mock_coro + return engine + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", mock_memory_engine): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + retain_chunk_size=5000, + ) + cerebral._bank_ensured = False + cerebral._ensure_bank() + + assert captured_chunk_size == 5000 + + def test_ensure_bank_called_once(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank is only called once.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + # Reset the mock to track only _ensure_bank calls + mock_run_async.reset_mock() + + # First call to _ensure_bank + cerebral._ensure_bank() + + # Second call should be a no-op + cerebral._ensure_bank() + + # ensure_bank_profile and update_bank_config should only be called once + assert mock_run_async.call_count == 2 # Once for ensure, once for update + + def test_ensure_bank_calls_profile_and_config(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank calls ensure_bank_profile and update_bank_config.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + bank_id="test-bank", + ) + + mock_run_async.reset_mock() + cerebral._bank_ensured = False + cerebral._ensure_bank() + + # Should call ensure_bank_profile and update_bank_config + assert mock_run_async.call_count == 2 + + def test_ensure_bank_sends_observation_scope_limits_default(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank sends observation_scope_limits with default cap (100).""" + captured_updates = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_updates + captured_updates = updates + return None + + mock_engine.update_bank_config = mock_update_bank_config + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", return_value=mock_engine), \ + patch("codespy.agents.memory.cerebral.cerebral.ensure_maintenance_routines", AsyncMock()): + c = Cerebral(database_url="postgresql://x/y", llm_provider="litellm") + c._ensure_bank() + c.close() + + assert captured_updates is not None + assert "observation_scope_limits" in captured_updates + limits = captured_updates["observation_scope_limits"] + assert len(limits) == 1 + assert limits[0]["scope"] == ["org:*", "repo:*", "project_scope:*"] + assert limits[0]["limit"] == 100 # default + + def test_ensure_bank_sends_observation_scope_limits_custom(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank sends observation_scope_limits with custom cap.""" + captured_updates = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_updates + captured_updates = updates + return None + + mock_engine.update_bank_config = mock_update_bank_config + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", return_value=mock_engine), \ + patch("codespy.agents.memory.cerebral.cerebral.ensure_maintenance_routines", AsyncMock()): + c = Cerebral( + database_url="postgresql://x/y", + llm_provider="litellm", + max_observations_per_scope=50, + ) + c._ensure_bank() + c.close() + + assert captured_updates is not None + assert "observation_scope_limits" in captured_updates + limits = captured_updates["observation_scope_limits"] + assert limits[0]["limit"] == 50 + + def test_ensure_bank_no_observation_scope_limits_when_unlimited(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that _ensure_bank skips observation_scope_limits when cap is -1 (unlimited).""" + captured_updates = None + + async def mock_update_bank_config(bank_id, updates, request_context): + nonlocal captured_updates + captured_updates = updates + return None + + mock_engine.update_bank_config = mock_update_bank_config + + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", return_value=mock_engine), \ + patch("codespy.agents.memory.cerebral.cerebral.ensure_maintenance_routines", AsyncMock()): + c = Cerebral( + database_url="postgresql://x/y", + llm_provider="litellm", + max_observations_per_scope=-1, + ) + c._ensure_bank() + c.close() + + assert captured_updates is not None + # Should NOT include observation_scope_limits when -1 + assert "observation_scope_limits" not in captured_updates + + +class TestCerebralRetainEpisode: + """Tests for Cerebral retain_episode method.""" + + def test_retain_episode_builds_correct_tags(self, mock_memory_engine_class, episode, mock_litellm): + """Test that tags are built correctly from episode. + + Only project_scope topics are included; pull_request, episode, and + run_id tags are no longer added. Repo/org tags are added when repo is known. + """ + with patch.object(Cerebral, "_run_async"): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + tags = cerebral._build_tags(episode, repo_full_name="test/repo") + + expected_tags = [ + "project_scope:test/repo/package", + "org:test", + "repo:test/repo", + "task:code_review", + ] + assert tags == expected_tags + # episode: and run_id: should NOT be present + assert not any(t.startswith(("episode:", "run_id:")) for t in tags) + # pull_request: should NOT be present + assert not any(t.startswith("pull_request:") for t in tags) + + def test_retain_episode_creates_contents_for_observations(self, mock_memory_engine_class, episode, mock_engine, mock_litellm): + """Test that retain_episode creates a single merged content item for all observations.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + mock_run_async.reset_mock() + cerebral._bank_ensured = True # Skip bank setup + + cerebral.retain_episode(episode) + + # Should call retain_batch_async + mock_run_async.assert_called_once() + + # Get the contents passed to retain_batch_async + call_args = mock_run_async.call_args + assert call_args is not None + + # Extract the contents argument from the coroutine call + # The first positional arg is the coroutine, we need to inspect it + coro = call_args[0][0] + # Access the bound arguments via the coroutine's __self__ if possible + # or check the call was made to retain_batch_async + engine_call = mock_engine.retain_batch_async + assert call_args is not None + + def test_retain_episode_creates_contents_for_artifacts(self, mock_memory_engine_class, episode, mock_litellm): + """Test that retain_episode creates a single merged content item for all artifacts.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + mock_run_async.reset_mock() + cerebral._bank_ensured = True + + cerebral.retain_episode(episode) + + # Should call retain_batch_async + mock_run_async.assert_called_once() + + def test_retain_episode_skips_when_no_contents(self, mock_memory_engine_class, mock_litellm): + """Test that retain_episode skips when there are no observations or artifacts.""" + empty_episode = Episode( + id=uuid.uuid4(), + run_id="test-run", + timestamp=datetime.now(UTC), + task="code_review", + module="CodeReviewer", + question="Review PR", + artifacts={}, + context_memory=ContextMemory(), + ) + + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + mock_run_async.reset_mock() + cerebral._bank_ensured = True + + cerebral.retain_episode(empty_episode) + + # Should NOT call retain_batch_async + mock_run_async.assert_not_called() + + def test_retain_episode_handles_exception_gracefully(self, mock_memory_engine_class, episode, mock_litellm): + """Test that retain_episode handles exceptions gracefully.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + cerebral._bank_ensured = True + + # Set side effect AFTER creating Cerebral (so initialize succeeds) + mock_run_async.side_effect = Exception("Network error") + + # Should not raise + cerebral.retain_episode(episode) + + def test_retain_episode_bundles_content_correctly(self, mock_memory_engine_class, episode, mock_litellm): + """Test that observations and artifacts are merged into at most 2 content items with proper headers.""" + captured_contents = [] + + # Create a mock engine that captures the contents passed to retain_batch_async + async def mock_retain_batch_async(*, bank_id, contents, request_context): + captured_contents.extend(contents) + return None + + # Create async mock coroutines for other methods + async def mock_coro(*args, **kwargs): + return None + + def mock_memory_engine(*args, **kwargs): + engine = MagicMock() + engine.initialize = mock_coro + engine.ensure_bank_profile = mock_coro + engine.update_bank_config = mock_coro + engine.retain_batch_async = mock_retain_batch_async + engine.close = mock_coro + return engine + + # Patch the MemoryEngine class at the module level + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", mock_memory_engine): + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + cerebral._bank_ensured = True + + cerebral.retain_episode(episode) + + # Episode has 2 observations (context_understanding, actions) + 1 artifact (review) + # Should be bundled into 2 content items max (observations + artifacts) + assert len(captured_contents) <= 2, f"Expected at most 2 content items, got {len(captured_contents)}" + assert len(captured_contents) >= 1, f"Expected at least 1 content item, got {len(captured_contents)}" + + # Find observations and artifacts content items + obs_item = None + art_item = None + for item in captured_contents: + # Check metadata.kind instead of context + if item.get("metadata", {}).get("kind") == "observation changes": + obs_item = item + elif item.get("metadata", {}).get("kind") == "artifacts": + art_item = item + + # Verify observations item + assert obs_item is not None, "Observations content item not found" + assert "[context_understanding]" in obs_item["content"], "Missing context_understanding header" + assert "[actions]" in obs_item["content"], "Missing actions header" + assert "This is a test observation" in obs_item["content"], "Missing observation content" + assert "Performed tool call" in obs_item["content"], "Missing action content" + # Verify metadata instead of context + assert "metadata" in obs_item, "Observations should have metadata" + assert "context" not in obs_item, "Observations should not have context" + assert obs_item["metadata"]["task"] == "code_review" + assert obs_item["metadata"]["kind"] == "observation changes" + assert obs_item["metadata"]["question"] == episode.question + + # Verify artifacts item + assert art_item is not None, "Artifacts content item not found" + assert "[artifact:review]" in art_item["content"], "Missing artifact header" + assert "# Review Results" in art_item["content"], "Missing artifact content" + # Verify metadata + assert "metadata" in art_item, "Artifacts should have metadata" + assert "context" not in art_item, "Artifacts should not have context" + assert art_item["metadata"]["task"] == "code_review" + assert art_item["metadata"]["kind"] == "artifacts" + + # Verify shared fields + expected_tags = cerebral._build_tags(episode, repo_full_name="test/repo") + expected_doc_id = f"episode-{episode.id}" + expected_event_date = episode.timestamp.isoformat() + + for item in captured_contents: + assert item["tags"] == expected_tags, f"Tags mismatch: {item['tags']} != {expected_tags}" + assert item["document_id"] == expected_doc_id, f"document_id mismatch" + assert item["event_date"] == expected_event_date, f"event_date mismatch" + + def test_retain_episode_uses_correct_document_id(self, mock_memory_engine_class, episode, mock_engine, mock_litellm): + """Test that retain_episode uses correct document_id format.""" + with patch.object(Cerebral, "_run_async") as mock_run_async: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + mock_run_async.reset_mock() + cerebral._bank_ensured = True + + cerebral.retain_episode(episode) + + # Get the contents passed to retain_batch_async + call_args = mock_run_async.call_args + assert call_args is not None + + +class TestCerebralRunAsync: + """Tests for Cerebral _run_async helper.""" + + def test_run_async_submits_to_dedicated_loop(self, mock_memory_engine_class, mock_litellm): + """Test _run_async submits coroutine to the dedicated event loop.""" + with mock_memory_engine_class: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + async def test_coro(): + return "result" + + result = cerebral._run_async(test_coro()) + assert result == "result" + # Verify the loop is running in a background thread + assert cerebral._loop_thread.is_alive() + assert cerebral._loop.is_running() + + def test_run_async_from_different_thread(self, mock_memory_engine_class, mock_litellm): + """Test _run_async is thread-safe when called from other threads.""" + import concurrent.futures + + with mock_memory_engine_class: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + async def test_coro(): + return "thread_result" + + # Submit from a different thread using ThreadPoolExecutor + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(cerebral._run_async, test_coro()) + result = future.result(timeout=5) + + assert result == "thread_result" + + +class TestCerebralClose: + """Tests for Cerebral close method.""" + + def test_close_stops_loop_and_joins_thread(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that close() stops the event loop and joins the thread.""" + with mock_memory_engine_class: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + # Verify loop is running before close + assert cerebral._loop_thread.is_alive() + assert cerebral._loop.is_running() + + cerebral.close() + + # After close, thread should be stopped + assert not cerebral._loop_thread.is_alive() + assert not cerebral._loop.is_running() + + def test_close_calls_engine_close(self, mock_memory_engine_class, mock_engine, mock_litellm): + """Test that close() calls engine.close().""" + with mock_memory_engine_class: + cerebral = Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + ) + + cerebral.close() + + # Verify engine.close() was called + mock_engine.close.assert_called_once() + + +class TestCerebralBuildTags: + """Tests for Cerebral _build_tags static method.""" + + def test_build_tags_with_multiple_topics(self): + """Test tag building with multiple topics.""" + episode = Episode( + id=uuid.uuid4(), + run_id="run-123", + timestamp=datetime.now(UTC), + task="scope", + module="ScopeResolver", + question="Resolve scopes", + artifacts={}, + context_memory=ContextMemory( + topics=[ + Topic(id="owner/repo", type="project_scope", description="Main scope"), + Topic(id="pr-123", type="pull_request", description="PR"), + ], + ), + ) + + tags = Cerebral._build_tags(episode) + + assert len(tags) == 5 + assert "project_scope:owner/repo" in tags + assert "pull_request:pr-123" in tags + assert f"episode:{episode.id}" in tags + assert "task:scope" in tags + assert "run_id:run-123" in tags + + def test_build_tags_with_no_topics(self): + """Test tag building with no topics.""" + episode = Episode( + id=uuid.uuid4(), + run_id="run-456", + timestamp=datetime.now(UTC), + task="code_review", + module="CodeReviewer", + question="Review code", + artifacts={}, + context_memory=ContextMemory(), + ) + + tags = Cerebral._build_tags(episode) + + assert len(tags) == 3 + assert f"episode:{episode.id}" in tags + assert "task:code_review" in tags + assert "run_id:run-456" in tags + + +def _mut(type_, obs_id, *, content=None, previous=None, section="context_understanding"): + from codespy.agents.memory.hippocampus.context_memory import MutationType + + return Mutation( + step=0, + type=MutationType(type_), + observation_id=obs_id, + section=section, + content=content, + previous_content=previous, + ) + + +def _episode_with(mutations, observations=()): + return Episode( + id=uuid.uuid4(), + run_id="r", + timestamp=datetime.now(UTC), + task="code_review", + module="m", + question="q", + artifacts={}, + context_memory=ContextMemory( + context_understanding=[Observation(id=i, content=c) for i, c in observations] + ), + mutations=list(mutations), + ) + + +class TestMutationLines: + """Cerebral retains the mutation log only.""" + + def test_add(self): + ep = _episode_with([_mut("ADD", "cu-a", content="new")], [("cu-a", "new")]) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] new"] + + def test_add_then_replace_uses_final_content(self): + ep = _episode_with( + [_mut("ADD", "cu-a", content="v1"), _mut("REPLACE", "cu-a", content="v2", previous="v1")], + [("cu-a", "v2")], + ) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] v2"] + + def test_add_then_delete_emits_nothing(self): + ep = _episode_with( + [_mut("ADD", "cu-a", content="v1"), _mut("DELETE", "cu-a", previous="v1")] + ) + assert Cerebral._mutation_lines(ep) == [] + + def test_prior_replace_supersedes(self): + ep = _episode_with( + [_mut("REPLACE", "cu-p", content="new", previous="old")], [("cu-p", "new")] + ) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] new (supersedes: old)"] + + def test_replace_then_replace_uses_first_previous(self): + ep = _episode_with( + [ + _mut("REPLACE", "cu-p", content="mid", previous="old"), + _mut("REPLACE", "cu-p", content="new", previous="mid"), + ], + [("cu-p", "new")], + ) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] new (supersedes: old)"] + + def test_prior_delete_retracted(self): + ep = _episode_with([_mut("DELETE", "cu-p", previous="wrong fact")]) + assert Cerebral._mutation_lines(ep) == [ + "[context_understanding] RETRACTED — shown incorrect or misleading, do not rely on: wrong fact" + ] + + def test_prior_evict_emits_nothing(self): + ep = _episode_with([_mut("EVICT", "cu-p", previous="still true")]) + assert Cerebral._mutation_lines(ep) == [] + + def test_replace_then_evict_emits_supersedes(self): + ep = _episode_with( + [ + _mut("REPLACE", "cu-p", content="new", previous="old"), + _mut("EVICT", "cu-p", previous="new"), + ] + ) + assert Cerebral._mutation_lines(ep) == [ + "[context_understanding] new (supersedes: old)" + ] + + def test_inherited_unchanged_observation_not_retained(self): + ep = _episode_with([], [("cu-p", "unchanged prior")]) + assert Cerebral._mutation_lines(ep) == [] + + def test_add_replace_evict_emits_last_content(self): + """ADD → REPLACE → EVICT gives a plain line with the last content.""" + ep = _episode_with( + [ + _mut("ADD", "cu-a", content="v1"), + _mut("REPLACE", "cu-a", content="v2", previous="v1"), + _mut("EVICT", "cu-a", previous="v2"), + ] + ) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] v2"] + + def test_evicted_add_emits_content(self): + """ADD → EVICT gives a plain line with the evicted content.""" + ep = _episode_with( + [_mut("ADD", "cu-a", content="gone"), _mut("EVICT", "cu-a", previous="gone")] + ) + assert Cerebral._mutation_lines(ep) == ["[context_understanding] gone"] + + +# --------------------------------------------------------------------------- +# Prefrontal-related Cerebral behaviour +# --------------------------------------------------------------------------- + +from unittest.mock import AsyncMock # noqa: E402 + +from hindsight_api.engine.memory_engine import Budget # noqa: E402 +from hindsight_api.engine.task_backend import SyncTaskBackend # noqa: E402 +from hindsight_api.extensions import OperationValidationError # noqa: E402 + +from codespy.agents.memory.cerebral.cerebral import ( # noqa: E402 + MENTAL_MODEL_PLACEHOLDER, + MENTAL_MODEL_SOURCE_QUERY, + OBSERVATIONS_MISSION, +) +from codespy.agents.memory.prefrontal.reach import mental_model_id # noqa: E402 + + +@pytest.fixture +def async_engine(): + """A MemoryEngine mock whose coroutine methods are AsyncMocks.""" + engine = MagicMock() + for name in ( + "initialize", + "ensure_bank_profile", + "update_bank_config", + "retain_batch_async", + "recall_async", + "reflect_async", + "get_mental_model", + "create_mental_model", + "submit_async_refresh_mental_model", + "get_observation_history", + "get_bank_profile", + "close", + ): + setattr(engine, name, AsyncMock(return_value=None)) + # Default get_bank_profile to return a profile so existing tests pass + engine.get_bank_profile.return_value = {"bank_id": "codebase"} + return engine + + +@pytest.fixture +def cerebral_async(async_engine, mock_litellm): + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", return_value=async_engine) as cls, \ + patch("codespy.agents.memory.cerebral.cerebral.ensure_maintenance_routines", AsyncMock()): + c = Cerebral(database_url="postgresql://localhost:5432/test", llm_provider="litellm") + c._bank_ensured = True + c._engine_cls = cls + yield c + c.close() + + +class TestCerebralTaskBackend: + def test_sync_task_backend_passed_to_engine(self, mock_memory_engine_class, mock_litellm): + Cerebral(database_url="postgresql://localhost:5432/test", llm_provider="litellm") + assert isinstance(mock_memory_engine_class.call_args.kwargs["task_backend"], SyncTaskBackend) + # Check that the subclass has the schema set correctly + backend = mock_memory_engine_class.call_args.kwargs["task_backend"] + assert backend._schema == "semantic" + + def test_schema_sync_task_backend_injects_schema(self): + """Test that _SchemaSyncTaskBackend injects _schema into task dicts.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task = {} + + async def mock_executor(task_dict): + received_task.update(task_dict) + + backend.set_executor(mock_executor) + + import asyncio + asyncio.run(backend.submit_task({"type": "consolidation"})) + + assert received_task.get("_schema") == "semantic" + + def test_schema_sync_task_backend_preserves_existing_schema(self): + """Test that _SchemaSyncTaskBackend preserves existing _schema values.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task = {} + + async def mock_executor(task_dict): + received_task.update(task_dict) + + backend.set_executor(mock_executor) + + import asyncio + asyncio.run(backend.submit_task({"type": "consolidation", "_schema": "other"})) + + assert received_task.get("_schema") == "other" + + def test_schema_sync_task_backend_does_not_mutate_original(self): + """Test that _SchemaSyncTaskBackend does not mutate the caller's task dict.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task = {} + + async def mock_executor(task_dict): + received_task.update(task_dict) + + backend.set_executor(mock_executor) + + original = {"type": "consolidation"} + original_copy = dict(original) + + import asyncio + asyncio.run(backend.submit_task(original)) + + assert original == original_copy # Original should be unchanged + assert received_task.get("_schema") == "semantic" + + def test_schema_sync_task_backend_has_attach_method(self): + """Test that _SchemaSyncTaskBackend has an attach method for engine binding.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + mock_engine = MagicMock() + + # attach should set _engine + backend.attach(mock_engine) + assert backend._engine is mock_engine + + def test_schema_sync_task_backend_catches_exception_and_re_raises(self): + """Test that _SchemaSyncTaskBackend catches exceptions, tries to mark failed, and re-raises.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + + async def failing_executor(task_dict): + raise RuntimeError("task failed") + + backend.set_executor(failing_executor) + + import asyncio + with pytest.raises(RuntimeError, match="task failed"): + asyncio.run(backend.submit_task({"type": "consolidation", "operation_id": "op-123"})) + + def test_error_message_uses_correct_env_var(self, mock_memory_engine_class, mock_litellm): + """Test that embedding probe error mentions MEMORY_EMBEDDINGS_MODEL.""" + import litellm + + with patch.object(litellm, "embedding", side_effect=Exception("API error")): + with pytest.raises(RuntimeError) as exc_info: + Cerebral(database_url="postgresql://localhost:5432/test", llm_provider="litellm") + + assert "MEMORY_EMBEDDINGS_MODEL" in str(exc_info.value) + + def test_schema_sync_task_backend_sets_contextvar_for_consolidation(self): + """Test that _SchemaSyncTaskBackend sets current_memory_task for consolidation.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + from codespy.agents.memory.cerebral.cost import current_memory_task + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task_type = [] + + async def mock_executor(task_dict): + received_task_type.append(current_memory_task.get()) + + backend.set_executor(mock_executor) + + import asyncio + asyncio.run(backend.submit_task({"type": "consolidation"})) + + assert received_task_type == ["consolidation"] + # After the call, the contextvar should be reset + assert current_memory_task.get() is None + + def test_schema_sync_task_backend_sets_contextvar_for_refresh_mental_model(self): + """Test that _SchemaSyncTaskBackend sets current_memory_task for refresh_mental_model.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + from codespy.agents.memory.cerebral.cost import current_memory_task + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task_type = [] + + async def mock_executor(task_dict): + received_task_type.append(current_memory_task.get()) + + backend.set_executor(mock_executor) + + import asyncio + asyncio.run(backend.submit_task({"type": "refresh_mental_model"})) + + assert received_task_type == ["refresh_mental_model"] + # After the call, the contextvar should be reset + assert current_memory_task.get() is None + + def test_schema_sync_task_backend_no_contextvar_for_batch_retain(self): + """Test that _SchemaSyncTaskBackend does not set current_memory_task for batch_retain.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + from codespy.agents.memory.cerebral.cost import current_memory_task + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + received_task_type = [] + + async def mock_executor(task_dict): + received_task_type.append(current_memory_task.get()) + + backend.set_executor(mock_executor) + + import asyncio + asyncio.run(backend.submit_task({"type": "batch_retain"})) + + assert received_task_type == [None] + # Contextvar should remain None + assert current_memory_task.get() is None + + def test_schema_sync_task_backend_resets_contextvar_on_exception(self): + """Test that _SchemaSyncTaskBackend resets current_memory_task even when executor raises.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + from codespy.agents.memory.cerebral.cost import current_memory_task + + backend = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + + async def failing_executor(task_dict): + raise RuntimeError("task failed") + + backend.set_executor(failing_executor) + + import asyncio + # Pre-set a value to verify it's restored + token = current_memory_task.set("outer_task") + try: + with pytest.raises(RuntimeError, match="task failed"): + asyncio.run(backend.submit_task({"type": "consolidation"})) + finally: + current_memory_task.reset(token) + + # After the call, the contextvar should be reset (back to None since we cleaned up) + assert current_memory_task.get() is None + + def test_schema_sync_task_backend_nested_tasks_innermost_wins(self): + """Test nested tasks: innermost task type wins, outer restored after inner returns.""" + from codespy.agents.memory.cerebral.cerebral import _SchemaSyncTaskBackend, HINDSIGHT_SCHEMA + from codespy.agents.memory.cerebral.cost import current_memory_task + + backend_outer = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + backend_inner = _SchemaSyncTaskBackend(HINDSIGHT_SCHEMA) + + inner_received = [] + + async def inner_executor(task_dict): + inner_received.append(current_memory_task.get()) + + async def outer_executor(task_dict): + # This simulates a consolidation task submitting a refresh_mental_model + if task_dict.get("type") == "consolidation": + backend_inner.set_executor(inner_executor) + await backend_inner.submit_task({"type": "refresh_mental_model"}) + # After inner returns, outer's task should be restored + + backend_outer.set_executor(outer_executor) + + import asyncio + asyncio.run(backend_outer.submit_task({"type": "consolidation"})) + + # Inner should see refresh_mental_model + assert inner_received == ["refresh_mental_model"] + # After everything, contextvar should be reset + assert current_memory_task.get() is None + + +class TestCerebralReflectModel: + def test_reflect_llm_forwarded_when_set(self, mock_memory_engine_class, mock_litellm): + Cerebral( + database_url="postgresql://localhost:5432/test", + llm_provider="litellm", + llm_model="openai/gpt-4o", + reflect_llm_model="anthropic/claude-haiku", + reflect_llm_api_key="sk-ant", + ) + kwargs = mock_memory_engine_class.call_args.kwargs + assert kwargs["memory_llm_model"] == "openai/gpt-4o" + assert kwargs["reflect_llm_provider"] == "litellm" + assert kwargs["reflect_llm_model"] == "anthropic/claude-haiku" + assert kwargs["reflect_llm_api_key"] == "sk-ant" + assert kwargs["reflect_llm_base_url"] is None + + def test_reflect_llm_omitted_by_default(self, mock_memory_engine_class, mock_litellm): + Cerebral(database_url="postgresql://localhost:5432/test", llm_provider="litellm") + kwargs = mock_memory_engine_class.call_args.kwargs + assert not any(k.startswith("reflect_llm_") for k in kwargs) + + +class TestCerebralObservationsMission: + def test_observations_mission_sent(self, mock_litellm): + captured = {} + + async def update_bank_config(bank_id, updates, request_context): + captured.update(updates) + + engine = MagicMock() + engine.initialize = AsyncMock() + engine.ensure_bank_profile = AsyncMock() + engine.update_bank_config = update_bank_config + engine.close = AsyncMock() + with patch("codespy.agents.memory.cerebral.cerebral.MemoryEngine", return_value=engine), \ + patch("codespy.agents.memory.cerebral.cerebral.ensure_maintenance_routines", AsyncMock()): + c = Cerebral(database_url="postgresql://x/y", llm_provider="litellm") + c._ensure_bank() + c.close() + assert captured["observations_mission"] == OBSERVATIONS_MISSION + assert "RETRACTED" in OBSERVATIONS_MISSION and "supersedes:" in OBSERVATIONS_MISSION + + +class TestCerebralRetainScopes: + def test_tags_with_repo(self, episode): + tags = Cerebral._build_tags(episode, "test/repo") + assert "repo:test/repo" in tags + assert "org:test" in tags + # episode: no longer added + assert f"episode:{episode.id}" not in tags + # pull_request: no longer added + assert "pull_request:https://github.com/test/repo/pull/1" not in tags + # task: still present + assert "task:code_review" in tags + + def test_tags_without_repo_unchanged(self, episode): + assert Cerebral._build_tags(episode) == Cerebral._build_tags(episode, None) + assert not any(t.startswith(("repo:", "org:")) for t in Cerebral._build_tags(episode)) + # episode: and run_id: should not be present + assert not any(t.startswith(("episode:", "run_id:")) for t in Cerebral._build_tags(episode)) + + def test_observation_scopes_per_project_scope(self, episode): + scopes = Cerebral._observation_scopes(episode, "test/repo") + assert scopes == [["org:test", "repo:test/repo", "project_scope:test/repo/package"]] + + def test_observation_scopes_without_project_scope(self): + """Fallback is now repo-root project scope, not bare [org:, repo:].""" + ep = Episode( + id=uuid.uuid4(), run_id="r", timestamp=datetime.now(UTC), task="summary", + module="m", question="q", artifacts={}, context_memory=ContextMemory(), + ) + # Fallback is now [org:, repo:, project_scope:o/r] (repo-root scope) + assert Cerebral._observation_scopes(ep, "o/r") == [ + ["org:o", "repo:o/r", "project_scope:o/r"] + ] + + def test_observation_scopes_none_without_repo(self, episode): + assert Cerebral._observation_scopes(episode, None) is None + + def test_scope_excludes_run_tags(self, episode): + for scope in Cerebral._observation_scopes(episode, "test/repo"): + # Scopes should only contain org:, repo:, project_scope: + assert all(t.startswith(("org:", "repo:", "project_scope:")) for t in scope) + # task:, episode:, run_id:, pull_request: should NOT be in scope + assert not any(t.startswith(("task:", "episode:", "run_id:", "pull_request:")) for t in scope) + + def test_retain_sets_observation_scopes_on_both_items(self, cerebral_async, async_engine, episode): + cerebral_async._mental_models = False + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + contents = async_engine.retain_batch_async.await_args.kwargs["contents"] + assert len(contents) == 2 + for item in contents: + assert item["observation_scopes"] == [ + ["org:test", "repo:test/repo", "project_scope:test/repo/package"] + ] + assert "repo:test/repo" in item["tags"] and "org:test" in item["tags"] + + def test_retain_without_repo_has_no_scopes(self, cerebral_async, async_engine, episode): + cerebral_async.retain_episode(episode) + contents = async_engine.retain_batch_async.await_args.kwargs["contents"] + assert all("observation_scopes" not in item for item in contents) + async_engine.get_mental_model.assert_not_awaited() + + def test_recalls_never_retained(self, cerebral_async, async_engine, episode): + """Episode.recalls is monitoring data: retain_episode must ignore it.""" + from codespy.agents.memory.recall import RecallRecord + + cerebral_async._mental_models = False + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + without = async_engine.retain_batch_async.await_args.kwargs["contents"] + + with_recalls = episode.model_copy( + update={ + "recalls": [ + RecallRecord(ordinal=0, kind="load", text="RECALLED-TEXT", status="ok") + ] + } + ) + cerebral_async.retain_episode(with_recalls, repo_full_name="test/repo") + with_ = async_engine.retain_batch_async.await_args.kwargs["contents"] + + assert with_ == without + assert all("RECALLED-TEXT" not in item["content"] for item in with_) + + +class TestConsolidateRun: + """One consolidation per run, submitted after all retains.""" + + def _engine(self, async_engine): + async_engine.submit_async_consolidation = AsyncMock(return_value={"operation_id": "op"}) + return async_engine + + def test_retain_does_not_consolidate(self, cerebral_async, async_engine, episode): + engine = self._engine(async_engine) + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + engine.submit_async_consolidation.assert_not_awaited() + assert episode.run_id in cerebral_async._pending_scopes + + def test_one_submit_with_union_of_scopes(self, cerebral_async, async_engine, episode): + engine = self._engine(async_engine) + other = episode.model_copy(update={"id": uuid.uuid4(), "task": "doc"}) + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + cerebral_async.retain_episode(other, repo_full_name="test/repo") + + cerebral_async.consolidate_run(episode.run_id) + + engine.submit_async_consolidation.assert_awaited_once() + scopes = engine.submit_async_consolidation.await_args.kwargs["observation_scopes"] + assert sorted(map(sorted, scopes)) == sorted( + map(sorted, [ + ["org:test", "repo:test/repo", "project_scope:test/repo/package"], + ["org:test", "repo:test/repo"], + ]) + ) + assert episode.run_id not in cerebral_async._pending_scopes + + def test_other_run_untouched(self, cerebral_async, async_engine, episode): + engine = self._engine(async_engine) + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + cerebral_async.consolidate_run("another-run") + engine.submit_async_consolidation.assert_not_awaited() + assert episode.run_id in cerebral_async._pending_scopes + + def test_failure_swallowed_and_pending_released(self, cerebral_async, async_engine, episode): + engine = self._engine(async_engine) + engine.submit_async_consolidation.side_effect = RuntimeError("boom") + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + cerebral_async.consolidate_run(episode.run_id) # must not raise + assert episode.run_id not in cerebral_async._pending_scopes + + +class TestEnsureMentalModels: + def test_ids_are_deterministic(self): + a = mental_model_id(["repo:o/r", "org:o"]) + b = mental_model_id(["org:o", "repo:o/r"]) + assert a == b + assert a.startswith("mm-") and len(a) == 3 + 32 + assert a != mental_model_id(["org:o", "repo:o/other"]) + + def test_retain_ensures_scope_and_repo_models(self, cerebral_async, async_engine, episode): + cerebral_async.retain_episode(episode, repo_full_name="test/repo") + created = [c.kwargs["mental_model_id"] for c in async_engine.create_mental_model.await_args_list] + assert created == [ + mental_model_id(["org:test", "repo:test/repo", "project_scope:test/repo/package"]), + mental_model_id(["org:test", "repo:test/repo"]), + ] + first = async_engine.create_mental_model.await_args_list[0] + assert first.args[1] == "Briefing: test/repo/package" + assert first.args[2] == MENTAL_MODEL_SOURCE_QUERY + assert first.args[3] == MENTAL_MODEL_PLACEHOLDER + assert first.kwargs["trigger"] == {"refresh_after_consolidation": True, "exclude_mental_models": True} + assert first.kwargs["max_tokens"] == 2048 + second = async_engine.create_mental_model.await_args_list[1] + assert second.args[1] == "Briefing: test/repo" + # No longer calls submit_async_refresh_mental_model - consolidation handles refresh + + def test_existing_model_not_created(self, cerebral_async, async_engine): + async_engine.get_mental_model.return_value = {"id": "x", "content": "c"} + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) + async_engine.create_mental_model.assert_not_awaited() + + def test_409_tolerated(self, cerebral_async, async_engine): + async_engine.create_mental_model.side_effect = OperationValidationError("exists", status_code=409) + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) + assert mental_model_id(["org:o", "repo:o/r"]) in cerebral_async._ensured_mental_models + + def test_second_call_served_from_cache(self, cerebral_async, async_engine): + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) + assert async_engine.get_mental_model.await_count == 1 + + def test_disabled(self, cerebral_async, async_engine, episode): + cerebral_async._mental_models = False + cerebral_async._sync_briefing_triggers([["org:test", "repo:test/repo"]]) + # When mental_models is False, get_mental_model is still called to check + # for existing briefings that need their trigger updated + async_engine.get_mental_model.assert_awaited() + # But create is not called when mental_models is False + async_engine.create_mental_model.assert_not_awaited() + + def test_failures_only_log(self, cerebral_async, async_engine): + async_engine.get_mental_model.side_effect = RuntimeError("db down") + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) # no raise + assert not cerebral_async._ensured_mental_models + + def test_non_409_validation_error_logged(self, cerebral_async, async_engine): + async_engine.create_mental_model.side_effect = OperationValidationError("forbidden", status_code=403) + cerebral_async._sync_briefing_triggers([["org:o", "repo:o/r"]]) # no raise + assert not cerebral_async._ensured_mental_models + + +class TestCerebralReadApi: + def test_arecall_forwards_arguments(self, cerebral_async, async_engine): + import asyncio as _asyncio + + fact = MagicMock() + async_engine.recall_async.return_value = MagicMock(results=[fact]) + when = datetime(2026, 9, 29, tzinfo=UTC) + groups = [object()] + facts = _asyncio.run( + cerebral_async.arecall( + "q", + tag_groups=groups, + max_tokens=123, + fact_type=["world", "observation"], + prefer_observations=True, + budget="high", + question_date=when, + ) + ) + assert facts == [fact] + call = async_engine.recall_async.await_args + assert call.args == ("codespy", "q") + assert call.kwargs["tag_groups"] is groups + assert call.kwargs["max_tokens"] == 123 + assert call.kwargs["fact_type"] == ["world", "observation"] + assert call.kwargs["prefer_observations"] is True + assert call.kwargs["budget"] == Budget.HIGH + assert call.kwargs["question_date"] == when + + def test_recall_sync(self, cerebral_async, async_engine): + async_engine.recall_async.return_value = MagicMock(results=[]) + assert cerebral_async.recall("q") == [] + + def test_areflect_uses_mid_budget(self, cerebral_async, async_engine): + import asyncio as _asyncio + + async_engine.reflect_async.return_value = MagicMock(text=" answer ") + text, summary = _asyncio.run( + cerebral_async.areflect("q", tag_groups=None, max_tokens=50, context="code_review on o/r") + ) + assert text == "answer" + assert summary.empty is False + call = async_engine.reflect_async.await_args + assert call.kwargs["budget"] == Budget.LOW + assert call.kwargs["max_tokens"] == 50 + assert call.kwargs["context"] == "code_review on o/r" + + def test_areflect_uses_low_budget(self, cerebral_async, async_engine): + """Test that areflect uses LOW budget (with 0.5x multiplier, cap is doubled).""" + import asyncio as _asyncio + + async_engine.reflect_async.return_value = MagicMock(text="answer") + text, summary = _asyncio.run( + cerebral_async.areflect( + "q", + tag_groups=None, + max_tokens=50, + context="code_review", + ) + ) + call = async_engine.reflect_async.await_args + assert call.kwargs["budget"] == Budget.LOW + + def test_areflect_basic(self, cerebral_async, async_engine): + """Test basic areflect call.""" + import asyncio as _asyncio + + async_engine.reflect_async.return_value = MagicMock(text="answer") + text, summary = _asyncio.run( + cerebral_async.areflect( + "q", + tag_groups=None, + max_tokens=50, + context="code_review", + ) + ) + assert text == "answer" + assert summary.empty is False + + def test_areflect_returns_reflect_summary(self, cerebral_async, async_engine): + import asyncio as _asyncio + + # Create a mock result with the real hindsight_api shape + # llm_trace is a list of objects with 'scope' attribute + # tool_trace is a list of objects with 'tool', 'input', 'output' attributes + from unittest.mock import Mock + + mock_result = MagicMock() + mock_result.text = "reflected answer" + + # Mock llm_trace as list of objects with 'scope' attribute + mock_result.llm_trace = [ + Mock(scope="agent_1"), + Mock(scope="final_map_1"), + Mock(scope="final_map_2"), + Mock(scope="final"), + Mock(scope="final_rewrite"), + ] + + # Mock tool_trace as list of objects with 'tool', 'output' attributes + mock_result.tool_trace = [ + Mock(tool="search_observations", input="q", output={"results": ["a"] * 50}), + Mock(tool="recall", input="q", output={"text": "x" * 100}), + ] + + # Mock usage as TokenUsage-like object + mock_result.usage = Mock(input_tokens=500, output_tokens=200, thoughts_tokens=50) + + async_engine.reflect_async.return_value = mock_result + text, summary = _asyncio.run(cerebral_async.areflect("q")) + + assert text == "reflected answer" + # iterations = agent_ scopes (1: agent_1) + 1 for final = 2 + assert summary.iterations == 2 + # llm_calls = 5 (all scopes) + assert summary.llm_calls == 5 + # map_calls = scopes starting with final_map_ (2) + assert summary.map_calls == 2 + # rewrite = True (has final_rewrite scope) + assert summary.rewrite is True + assert summary.empty is False + # tools = [(name, output_tokens)] from tool_trace + assert len(summary.tools) == 2 + assert summary.tools[0][0] == "search_observations" + assert summary.tools[1][0] == "recall" + # usage contains token counts + assert summary.usage["input_tokens"] == 500 + assert summary.usage["output_tokens"] == 200 + assert summary.usage["thoughts_tokens"] == 50 + + def test_aget_mental_models_skips_missing(self, cerebral_async, async_engine): + import asyncio as _asyncio + + async def get(bank_id, mm_id, request_context): + return None if mm_id == "b" else {"id": mm_id} + + async_engine.get_mental_model.side_effect = get + models = _asyncio.run(cerebral_async.aget_mental_models(["a", "b", "c"])) + assert [m["id"] for m in models] == ["a", "c"] + + def test_aget_observation_history(self, cerebral_async, async_engine): + import asyncio as _asyncio + + async_engine.get_observation_history.return_value = [{"previous_text": "x"}] + assert _asyncio.run(cerebral_async.aget_observation_history("id-1")) == [{"previous_text": "x"}] + assert async_engine.get_observation_history.await_args.args == ("codespy", "id-1") + async_engine.get_observation_history.return_value = None + assert _asyncio.run(cerebral_async.aget_observation_history("id-2")) == [] + + +class TestCerebralMissingBank: + """Tests for Cerebral read behavior when bank does not exist yet.""" + + def test_arecall_returns_empty_when_bank_missing(self, cerebral_async, async_engine): + """arecall returns [] and does not call recall_async when bank is missing.""" + import asyncio as _asyncio + + async_engine.get_bank_profile.return_value = None + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + facts = _asyncio.run(cerebral_async.arecall("q")) + assert facts == [] + async_engine.recall_async.assert_not_awaited() + + def test_areflect_returns_empty_when_bank_missing(self, cerebral_async, async_engine): + """areflect returns ("", summary) with empty=True when bank is missing.""" + import asyncio as _asyncio + + async_engine.get_bank_profile.return_value = None + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + text, summary = _asyncio.run(cerebral_async.areflect("q")) + assert text == "" + assert summary.empty is True + assert summary.llm_calls == 0 + async_engine.reflect_async.assert_not_awaited() + + def test_aget_mental_models_returns_empty_when_bank_missing(self, cerebral_async, async_engine): + """aget_mental_models returns [] when bank is missing.""" + import asyncio as _asyncio + + async_engine.get_bank_profile.return_value = None + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + models = _asyncio.run(cerebral_async.aget_mental_models(["a", "b"])) + assert models == [] + async_engine.get_mental_model.assert_not_awaited() + + def test_aget_observation_history_returns_empty_when_bank_missing(self, cerebral_async, async_engine): + """aget_observation_history returns [] when bank is missing.""" + import asyncio as _asyncio + + async_engine.get_bank_profile.return_value = None + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + history = _asyncio.run(cerebral_async.aget_observation_history("id-1")) + assert history == [] + async_engine.get_observation_history.assert_not_awaited() + + def test_missing_not_cached_exists_becomes_true(self, cerebral_async, async_engine): + """Missing is not cached: when bank is created, subsequent reads work.""" + import asyncio as _asyncio + + # First call: bank missing + async_engine.get_bank_profile.return_value = None + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + facts = _asyncio.run(cerebral_async.arecall("q")) + assert facts == [] + assert async_engine.get_bank_profile.await_count == 1 + + # Second call: bank now exists (e.g., retain_episode was called) + async_engine.get_bank_profile.return_value = {"bank_id": "codebase"} + # _bank_exists is still False because we didn't call _ensure_bank + cerebral_async._bank_exists = False + + # Reset recall_async mock to track new calls + async_engine.recall_async.return_value = MagicMock(results=[MagicMock()]) + + facts = _asyncio.run(cerebral_async.arecall("q")) + # Should have called get_bank_profile again and now recall_async + assert async_engine.get_bank_profile.await_count == 2 + async_engine.recall_async.assert_awaited_once() + + def test_exists_is_cached_no_duplicate_checks(self, cerebral_async, async_engine): + """Exists is cached: multiple arecalls only check once.""" + import asyncio as _asyncio + + async_engine.get_bank_profile.return_value = {"bank_id": "codebase"} + cerebral_async._bank_exists = False + cerebral_async._missing_bank_logged = False + + async_engine.recall_async.return_value = MagicMock(results=[]) + + # Two arecalls + _asyncio.run(cerebral_async.arecall("q1")) + _asyncio.run(cerebral_async.arecall("q2")) + + # get_bank_profile should only be awaited once + assert async_engine.get_bank_profile.await_count == 1 + # recall_async should be awaited twice + assert async_engine.recall_async.await_count == 2 + + def test_ensure_bank_sets_bank_exists(self, cerebral_async, async_engine): + """_ensure_bank success sets _bank_exists so get_bank_profile is not called.""" + cerebral_async._bank_exists = False + cerebral_async._bank_ensured = False + + cerebral_async._ensure_bank() + + assert cerebral_async._bank_exists is True + # After _ensure_bank, arecall should not call get_bank_profile + async_engine.get_bank_profile.reset_mock() + async_engine.recall_async.return_value = MagicMock(results=[]) + + import asyncio as _asyncio + _asyncio.run(cerebral_async.arecall("q")) + + async_engine.get_bank_profile.assert_not_awaited() + async_engine.recall_async.assert_awaited_once() diff --git a/tests/test_cerebral_cost.py b/tests/test_cerebral_cost.py new file mode 100644 index 0000000..718231a --- /dev/null +++ b/tests/test_cerebral_cost.py @@ -0,0 +1,778 @@ +"""Tests for the Cerebral cost tracking module.""" + +import asyncio +from unittest.mock import MagicMock, Mock, patch + +import pytest + +pytest.importorskip("hindsight_api") + +from codespy.agents.cost_tracker import CostTracker, get_cost_tracker +from codespy.agents.memory.cerebral.cost import ( + BUCKET_MEMORY_CONSOLIDATION, + BUCKET_MEMORY_EMBEDDINGS, + BUCKET_MEMORY_MENTAL_MODELS, + BUCKET_MEMORY_OTHER, + BUCKET_MEMORY_RECALL, + BUCKET_MEMORY_RECALL_EMBEDDINGS, + BUCKET_MEMORY_RETAIN, + CerebralCostRecorder, + MeteredLiteLLMSDKEmbeddings, + _LiteLLMProxy, + _llm_bucket, + _recorder_registered, + current_memory_task, + register_cerebral_cost_recorder, + unregister_cerebral_cost_recorder, +) +from codespy.agents.memory.recall import current_recall_usage, track_recall_usage + + +@pytest.fixture +def fresh_recorder(): + """Reset registration state before/after each test.""" + # Reset module state + from codespy.agents.memory.cerebral import cost as cost_module + + cost_module._recorder_registered = False + cost_module._warned_unpriced_models.clear() + + yield + + # Cleanup after test + try: + unregister_cerebral_cost_recorder() + except Exception: + pass + cost_module._recorder_registered = False + cost_module._warned_unpriced_models.clear() + + +@pytest.fixture +def mock_tracker(): + """Create a fresh CostTracker for tests.""" + tracker = CostTracker() + tracker.reset() + return tracker + + +@pytest.fixture +def mock_cost_recorder(): + """Create a mock CerebralCostRecorder.""" + return CerebralCostRecorder() + + +class TestCerebralCostRecorder: + """Tests for CerebralCostRecorder class.""" + + def test_record_llm_call_with_retain_scope(self, mock_tracker, fresh_recorder): + """scope='retain_extract_facts' → memory_retain bucket.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object( + CerebralCostRecorder, "_price_call_split", return_value=(0.03, 0.02) + ): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats is not None + assert stats.cost == 0.05 # 0.03 + 0.02 + assert stats.tokens == 150 + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + assert stats.input_cost == 0.03 + assert stats.output_cost == 0.02 + + def test_record_llm_call_with_consolidation_scope(self, mock_tracker, fresh_recorder): + """scope='consolidation' → memory_consolidation bucket (scope fallback, no tag).""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="consolidation", + input_tokens=100, + output_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_CONSOLIDATION) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + + def test_record_llm_call_with_mental_model_scope(self, mock_tracker, fresh_recorder): + """scope='mental_model_delta_ops' → memory_mental_models bucket (scope fallback, no tag).""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="mental_model_delta_ops", + input_tokens=100, + output_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_MENTAL_MODELS) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + + def test_record_llm_call_with_consolidation_task_tag(self, mock_tracker, fresh_recorder): + """current_memory_task='consolidation' with scope 'reflect' → memory_consolidation bucket.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + token = current_memory_task.set("consolidation") + try: + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="reflect", + input_tokens=100, + output_tokens=50, + ) + finally: + current_memory_task.reset(token) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_CONSOLIDATION) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + + def test_record_llm_call_with_refresh_mental_model_task_tag(self, mock_tracker, fresh_recorder): + """current_memory_task='refresh_mental_model' with scope 'reflect' → memory_mental_models bucket.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + token = current_memory_task.set("refresh_mental_model") + try: + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="reflect", + input_tokens=100, + output_tokens=50, + ) + finally: + current_memory_task.reset(token) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_MENTAL_MODELS) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + + def test_record_llm_call_recall_wins_over_task_tag(self, mock_tracker, fresh_recorder): + """current_recall_usage set AND current_memory_task set → memory_recall (recall wins).""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + with track_recall_usage() as usage: + token = current_memory_task.set("consolidation") + try: + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="reflect", + input_tokens=100, + output_tokens=50, + ) + finally: + current_memory_task.reset(token) + + # Should go to prefrontal, not consolidation + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RECALL) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + # Consolidation should be empty + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_CONSOLIDATION) is None + + def test_record_llm_call_task_tag_wins_over_retain_scope(self, mock_tracker, fresh_recorder): + """current_memory_task='consolidation' with scope 'retain_extract_facts' → memory_consolidation (tag wins).""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.02, 0.01)): + token = current_memory_task.set("consolidation") + try: + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + ) + finally: + current_memory_task.reset(token) + + # Task tag should win over scope + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_CONSOLIDATION) + assert stats is not None + assert stats.input_tokens == 100 + assert stats.output_tokens == 50 + # Retain should be empty + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) is None + + def test_record_llm_call_with_other_scope(self, mock_tracker, fresh_recorder): + """Any other scope → memory_other bucket.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.005, 0.005)): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="reflect", + input_tokens=100, + output_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_OTHER) + assert stats is not None + + def test_record_llm_call_skips_error_calls(self, mock_tracker, fresh_recorder): + """A call with error set → nothing recorded.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + error=Exception("API error"), + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats is None + + def test_record_llm_call_skips_zero_tokens(self, mock_tracker, fresh_recorder): + """Calls with zero tokens → nothing recorded.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=0, + output_tokens=0, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats is None + + def test_record_llm_call_prices_using_litellm(self, mock_tracker, fresh_recorder): + """Verify pricing uses litellm.cost_per_token.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch("litellm.cost_per_token", return_value=(0.01, 0.02)) as mock_cost: + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + ) + + mock_cost.assert_called_once_with( + model="openai/gpt-4", + prompt_tokens=100, + completion_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats.cost == 0.03 # 0.01 + 0.02 + + def test_record_llm_call_forwards_duration(self, mock_tracker, fresh_recorder): + """duration parameter is forwarded to add_external_call.""" + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.03, 0.02)): + recorder = CerebralCostRecorder() + recorder.record_llm_call( + model="openai/gpt-4", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + duration=1.25, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats is not None + assert stats.external_duration_seconds == 1.25 + assert stats.duration_seconds == 1.25 + + def test_record_llm_call_logs_warning_for_unpriced_model( + self, mock_tracker, fresh_recorder, caplog + ): + """An unpriced model → tokens recorded, $0, a single warning.""" + import logging + + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch( + "litellm.cost_per_token", side_effect=Exception("No price") + ): + recorder = CerebralCostRecorder() + with caplog.at_level(logging.WARNING, logger="codespy.agents.memory.cerebral.cost"): + # First call should warn + recorder.record_llm_call( + model="custom/model", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + ) + # Second call should NOT warn (already warned) + recorder.record_llm_call( + model="custom/model", + scope="retain_extract_facts", + input_tokens=100, + output_tokens=50, + ) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_RETAIN) + assert stats.cost == 0.0 + assert stats.tokens == 300 # 150 * 2 + # Should only have one warning in the log + assert caplog.text.count("No litellm price for model") == 1 + + +class TestRecallUsageAttribution: + """LLM/embedding calls inside track_recall_usage() are billed to memory_recall.""" + + def test_llm_call_inside_recall_goes_to_prefrontal(self, mock_tracker, fresh_recorder): + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.03, 0.02)): + with track_recall_usage() as usage: + CerebralCostRecorder().record_llm_call( + model="openai/gpt-4o", + scope="reflect", + input_tokens=100, + output_tokens=40, + ) + assert usage.llm_calls == 1 + assert usage.input_tokens == 100 and usage.output_tokens == 40 + assert usage.input_cost == 0.03 and usage.output_cost == 0.02 + assert usage.to_model_field() == "openai/gpt-4o" + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_RECALL).cost == 0.05 + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_OTHER) is None + + def test_llm_call_outside_recall_unchanged(self, mock_tracker, fresh_recorder): + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + with patch.object(CerebralCostRecorder, "_price_call_split", return_value=(0.01, 0.01)): + CerebralCostRecorder().record_llm_call( + model="openai/gpt-4o", scope="reflect", input_tokens=10, output_tokens=5 + ) + assert current_recall_usage.get() is None + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_OTHER) is not None + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_RECALL) is None + + @pytest.mark.asyncio + async def test_embedding_inside_recall_goes_to_prefrontal_embeddings(self, mock_tracker): + response = MagicMock() + response.usage = MagicMock() + response.usage.prompt_tokens = 12 + response._hidden_params = {"response_cost": 0.001} + real = MagicMock() + real.aembedding.return_value = asyncio.Future() + real.aembedding.return_value.set_result(response) + + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + proxy = _LiteLLMProxy(real, CerebralCostRecorder()) + with track_recall_usage() as usage: + await proxy.aembedding(model="openai/text-embedding-3-small", input=["q"]) + + assert usage.input_tokens == 12 and usage.output_tokens == 0 + assert usage.input_cost == 0.001 + assert usage.llm_calls == 0 # embeddings are not LLM calls + # Embeddings during active recall go to the new prefrontal_embeddings bucket + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_RECALL_EMBEDDINGS).tokens == 12 + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_RECALL) is None + assert mock_tracker.get_signature_stats(BUCKET_MEMORY_EMBEDDINGS) is None + + def test_usage_propagates_to_cross_thread_loop(self): + """Cerebral._await submits to its own loop thread: the contextvar must follow.""" + import threading + + loop = asyncio.new_event_loop() + thread = threading.Thread(target=loop.run_forever, daemon=True) + thread.start() + try: + + async def read_usage(): + return current_recall_usage.get() + + with track_recall_usage() as usage: + seen = asyncio.run_coroutine_threadsafe(read_usage(), loop).result(timeout=5) + assert seen is usage + after = asyncio.run_coroutine_threadsafe(read_usage(), loop).result(timeout=5) + assert after is None + finally: + loop.call_soon_threadsafe(loop.stop) + thread.join(timeout=5) + + +class TestLiteLLMProxy: + """Tests for _LiteLLMProxy class.""" + + @pytest.fixture + def mock_litellm(self): + """Create a mock litellm module.""" + return MagicMock() + + @pytest.fixture + def mock_response(self): + """Create a mock embedding response.""" + response = MagicMock() + response.usage = MagicMock() + response.usage.prompt_tokens = 100 + response._hidden_params = {"response_cost": 0.01} + return response + + @pytest.mark.asyncio + async def test_aembedding_meters_usage(self, mock_litellm, mock_response, mock_tracker): + """Embedding call meters usage to memory_embeddings bucket.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + cost_recorder = CerebralCostRecorder() + proxy = _LiteLLMProxy(mock_litellm, cost_recorder) + await proxy.aembedding(model="openai/text-embedding-3-small", input=["test"]) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_EMBEDDINGS) + assert stats is not None + assert stats.tokens == 100 + assert stats.cost == 0.01 + + @pytest.mark.asyncio + async def test_aembedding_records_duration(self, mock_litellm, mock_response, mock_tracker): + """Embedding call records nonzero duration to memory_embeddings bucket.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + with patch("codespy.agents.cost_tracker.get_cost_tracker", return_value=mock_tracker): + cost_recorder = CerebralCostRecorder() + proxy = _LiteLLMProxy(mock_litellm, cost_recorder) + await proxy.aembedding(model="openai/text-embedding-3-small", input=["test"]) + + stats = mock_tracker.get_signature_stats(BUCKET_MEMORY_EMBEDDINGS) + assert stats is not None + assert stats.external_duration_seconds >= 0.0 # Should have some duration (could be very small) + assert stats.duration_seconds == stats.external_duration_seconds + + @pytest.mark.asyncio + async def test_aembedding_returns_response_unchanged(self, mock_litellm, mock_response): + """Embedding call returns the response unchanged.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + cost_recorder = CerebralCostRecorder() + proxy = _LiteLLMProxy(mock_litellm, cost_recorder) + result = await proxy.aembedding(model="openai/text-embedding-3-small", input=["test"]) + + assert result is mock_response + + def test_getattr_forwards_other_attributes(self, mock_litellm): + """Other attributes forward to real litellm.""" + mock_litellm.some_attr = "value" + cost_recorder = CerebralCostRecorder() + proxy = _LiteLLMProxy(mock_litellm, cost_recorder) + + assert proxy.some_attr == "value" + + +class TestMeteredLiteLLMSDKEmbeddings: + """Tests for MeteredLiteLLMSDKEmbeddings class.""" + + @pytest.fixture + def mock_base_embeddings(self): + """Create a mock LiteLLMSDKEmbeddings.""" + base = MagicMock() + base._litellm = MagicMock() + base.initialize = MagicMock(return_value=asyncio.Future()) + base.initialize.return_value.set_result(None) + return base + + @pytest.fixture + def mock_cost_recorder(self): + """Create a mock CerebralCostRecorder.""" + return CerebralCostRecorder() + + @pytest.mark.asyncio + async def test_initialize_wraps_litellm_after_base_init( + self, mock_base_embeddings, mock_cost_recorder + ): + """Initialize wraps _litellm after base initialization.""" + metered = MeteredLiteLLMSDKEmbeddings( + base=mock_base_embeddings, cost_recorder=mock_cost_recorder + ) + + await metered.initialize() + + # Base initialize should have been called + mock_base_embeddings.initialize.assert_called_once() + # _litellm should now be wrapped + assert isinstance(mock_base_embeddings._litellm, _LiteLLMProxy) + + @pytest.mark.asyncio + async def test_initialize_skips_if_already_set( + self, mock_base_embeddings, mock_cost_recorder + ): + """Initialize skips wrapping if already wrapped.""" + metered = MeteredLiteLLMSDKEmbeddings( + base=mock_base_embeddings, cost_recorder=mock_cost_recorder + ) + + await metered.initialize() + first_proxy = mock_base_embeddings._litellm + + # Second initialize should not create a new proxy + await metered.initialize() + + assert mock_base_embeddings._litellm is first_proxy + + def test_getattr_forwards_to_base(self, mock_base_embeddings, mock_cost_recorder): + """Other attributes forward to base embeddings.""" + mock_base_embeddings.some_attr = "value" + metered = MeteredLiteLLMSDKEmbeddings( + base=mock_base_embeddings, cost_recorder=mock_cost_recorder + ) + + assert metered.some_attr == "value" + + +class TestRegisterCerebralCostRecorder: + """Tests for register_cerebral_cost_recorder function.""" + + def test_registration_is_idempotent(self, fresh_recorder): + """Registration is idempotent (one entry in _recorders).""" + with patch("hindsight_api.tracing.register_span_recorder") as mock_register: + recorder1 = register_cerebral_cost_recorder() + recorder2 = register_cerebral_cost_recorder() + + # Should return same recorder (or at least register once) + assert isinstance(recorder1, CerebralCostRecorder) + assert isinstance(recorder2, CerebralCostRecorder) + + def test_registers_with_span_recorder(self, fresh_recorder): + """Registers with Hindsight span recorder.""" + with patch("hindsight_api.tracing.register_span_recorder") as mock_register: + recorder = register_cerebral_cost_recorder() + + mock_register.assert_called_once() + assert mock_register.call_args[0][0] is recorder + + +class TestUnregisterCerebralCostRecorder: + """Tests for unregister_cerebral_cost_recorder function.""" + + def test_unregisters_specific_recorder(self, fresh_recorder): + """Unregister specific recorder.""" + with patch("hindsight_api.tracing.register_span_recorder"): + with patch("hindsight_api.tracing.unregister_span_recorder") as mock_unregister: + recorder = register_cerebral_cost_recorder() + unregister_cerebral_cost_recorder(recorder) + + mock_unregister.assert_called_once_with(recorder) + + def test_unregisters_any_cerebral_recorder(self, fresh_recorder): + """Unregister finds and removes any CerebralCostRecorder.""" + recorder = CerebralCostRecorder() + mock_span_recorder = MagicMock() + mock_span_recorder._recorders = [recorder] + + with patch("hindsight_api.tracing.get_span_recorder", return_value=mock_span_recorder): + with patch("hindsight_api.tracing.unregister_span_recorder") as mock_unregister: + unregister_cerebral_cost_recorder() + + mock_unregister.assert_called_once_with(recorder) + + +class TestPinTests: + """Pin tests for Hindsight internals we rely on.""" + + def test_lite_llm_sdke_has_litellm_attribute(self): + """LiteLLMSDKEmbeddings uses self._litellm attribute.""" + from hindsight_api.engine.embeddings import LiteLLMSDKEmbeddings + + # Check that LiteLLMSDKEmbeddings has _litellm attribute + embeddings = LiteLLMSDKEmbeddings.__new__(LiteLLMSDKEmbeddings) + assert hasattr(embeddings, "_litellm") + + def test_lite_llm_sdke_has_aembedding_method(self): + """LiteLLMSDKEmbeddings._litellm has aembedding method.""" + import litellm + + assert hasattr(litellm, "aembedding") + + def test_tracing_has_span_recorder(self): + """hindsight_api.tracing has get_span_recorder and register_span_recorder.""" + from hindsight_api import tracing + + assert hasattr(tracing, "get_span_recorder") + assert hasattr(tracing, "register_span_recorder") + assert hasattr(tracing, "unregister_span_recorder") + + def test_composite_span_recorder_has_recorders_list(self): + """CompositeSpanRecorder has _recorders list.""" + from hindsight_api.tracing import CompositeSpanRecorder + + recorder = CompositeSpanRecorder() + assert hasattr(recorder, "_recorders") + assert isinstance(recorder._recorders, list) + + def test_llm_span_recorder_accepts_duration_param(self): + """LLMSpanRecorder.record_llm_call accepts duration parameter (upstream contract).""" + import inspect + from hindsight_api.tracing import LLMSpanRecorder + + sig = inspect.signature(LLMSpanRecorder.record_llm_call) + assert "duration" in sig.parameters, "record_llm_call must accept 'duration' parameter" + + +class TestResolveEmbeddingInputLimits: + """Tests for resolve_embedding_input_limits function.""" + + def test_bedrock_cohere_multilingual_v3_returns_2048_and_truncate(self): + """Bedrock Cohere multilingual v3 returns 2048 and truncate=END.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("bedrock/cohere.embed-multilingual-v3") + assert max_chars == 2048 + assert extra == {"truncate": "END"} + + def test_bedrock_cohere_english_v3_returns_2048_and_truncate(self): + """Bedrock Cohere english v3 returns 2048 and truncate=END.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("bedrock/cohere.embed-english-v3") + assert max_chars == 2048 + assert extra == {"truncate": "END"} + + def test_region_prefixed_bedrock_cohere_matches(self): + """Region-prefixed Bedrock Cohere models also match.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("bedrock/eu.cohere.embed-multilingual-v3") + assert max_chars == 2048 + assert extra == {"truncate": "END"} + + def test_openai_returns_none_and_empty(self): + """OpenAI models return None and empty dict.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("openai/text-embedding-3-small") + assert max_chars is None + assert extra == {} + + def test_override_0_disables_cap(self): + """Override=0 disables character capping.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("bedrock/cohere.embed-multilingual-v3", override=0) + assert max_chars is None + assert extra == {} + + def test_override_n_uses_custom_cap(self): + """Override=N uses custom character cap.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("bedrock/cohere.embed-multilingual-v3", override=3000) + assert max_chars == 3000 + assert extra == {"truncate": "END"} + + def test_custom_cap_for_non_cohere_returns_empty_extra(self): + """Custom cap for non-Cohere models returns empty extra.""" + from codespy.agents.memory.cerebral.cost import resolve_embedding_input_limits + + max_chars, extra = resolve_embedding_input_limits("openai/text-embedding-3-small", override=3000) + assert max_chars == 3000 + assert extra == {} + + +class TestLiteLLMProxyInputCapping: + """Tests for _LiteLLMProxy input capping.""" + + @pytest.fixture + def mock_litellm(self): + """Create a mock litellm module.""" + return MagicMock() + + @pytest.fixture + def mock_response(self): + """Create a mock embedding response.""" + response = MagicMock() + response.usage = MagicMock() + response.usage.prompt_tokens = 100 + response._hidden_params = {"response_cost": 0.01} + return response + + @pytest.mark.asyncio + async def test_no_truncation_when_under_cap(self, mock_litellm, mock_response): + """Input under cap is not truncated.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + from codespy.agents.memory.cerebral.cost import _LiteLLMProxy, CerebralCostRecorder + + proxy = _LiteLLMProxy(mock_litellm, CerebralCostRecorder(), max_input_chars=100) + await proxy.aembedding(model="test", input=["short"]) + + call_args = mock_litellm.aembedding.call_args + assert call_args.kwargs["input"] == ["short"] + + @pytest.mark.asyncio + async def test_truncation_when_over_cap(self, mock_litellm, mock_response): + """Input over cap is truncated.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + from codespy.agents.memory.cerebral.cost import _LiteLLMProxy, CerebralCostRecorder + + proxy = _LiteLLMProxy(mock_litellm, CerebralCostRecorder(), max_input_chars=5) + await proxy.aembedding(model="test", input=["this is a long text"]) + + call_args = mock_litellm.aembedding.call_args + assert call_args.kwargs["input"] == ["this "] + + @pytest.mark.asyncio + async def test_does_not_mutate_original_list(self, mock_litellm, mock_response): + """Original input list is not mutated.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + from codespy.agents.memory.cerebral.cost import _LiteLLMProxy, CerebralCostRecorder + + original = ["this is a long text"] + original_copy = list(original) + + proxy = _LiteLLMProxy(mock_litellm, CerebralCostRecorder(), max_input_chars=5) + await proxy.aembedding(model="test", input=original) + + assert original == original_copy + + @pytest.mark.asyncio + async def test_extra_kwargs_forwarded(self, mock_litellm, mock_response): + """Extra kwargs are forwarded to the embedding call.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + from codespy.agents.memory.cerebral.cost import _LiteLLMProxy, CerebralCostRecorder + + proxy = _LiteLLMProxy( + mock_litellm, CerebralCostRecorder(), max_input_chars=100, extra_kwargs={"truncate": "END"} + ) + await proxy.aembedding(model="test", input=["short"]) + + call_args = mock_litellm.aembedding.call_args + assert call_args.kwargs["truncate"] == "END" + + @pytest.mark.asyncio + async def test_explicit_caller_kwargs_win_over_extra(self, mock_litellm, mock_response): + """Explicit caller kwargs win over extra_kwargs.""" + mock_litellm.aembedding.return_value = asyncio.Future() + mock_litellm.aembedding.return_value.set_result(mock_response) + + from codespy.agents.memory.cerebral.cost import _LiteLLMProxy, CerebralCostRecorder + + proxy = _LiteLLMProxy( + mock_litellm, CerebralCostRecorder(), max_input_chars=100, extra_kwargs={"truncate": "END"} + ) + await proxy.aembedding(model="test", input=["short"], truncate="START") + + call_args = mock_litellm.aembedding.call_args + assert call_args.kwargs["truncate"] == "START" diff --git a/tests/test_cerebral_delta_ops.py b/tests/test_cerebral_delta_ops.py new file mode 100644 index 0000000..ba5b101 --- /dev/null +++ b/tests/test_cerebral_delta_ops.py @@ -0,0 +1,346 @@ +"""Tests for delta operations coercion shim. + +These tests verify that the delta_ops_compat module correctly handles +JSON-encoded operations strings from models without native structured output. +""" + +from __future__ import annotations + +import json + +# Import hindsight_api first (available in test environment with venv) +from hindsight_api.engine.reflect import delta_ops + +# Now import the module under test +from codespy.agents.memory.cerebral.delta_ops_compat import ( + _decode_operation_strings, + _normalize, + _normalize_dict_operations, + install_delta_ops_coercion, +) + + +class TestNormalizeDictOperations: + """Tests for _normalize_dict_operations.""" + + def test_operations_as_json_string(self): + """Dict with operations as JSON string -> decoded to list.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + raw = {"operations": json.dumps([op])} + + result, changed = _normalize_dict_operations(raw) + + assert changed is True + assert result["operations"] == [op] + + def test_operations_as_nested_json_string(self): + """Dict with operations as nested JSON object -> inner list used.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + inner = {"operations": [op]} + raw = {"operations": json.dumps(inner)} + + result, changed = _normalize_dict_operations(raw) + + assert changed is True + assert result["operations"] == [op] + + def test_operations_as_list_of_json_strings(self): + """Dict with operations as list of JSON strings -> decoded to dicts.""" + op1 = {"op": "append_block", "section_id": "s1", "text": "hello"} + op2 = {"op": "replace_block", "section_id": "s1", "block_id": "b1", "text": "world"} + raw = {"operations": [json.dumps(op1), json.dumps(op2)]} + + result, changed = _normalize_dict_operations(raw) + + assert changed is True + assert result["operations"] == [op1, op2] + + def test_well_formed_dict_unchanged(self): + """Well-formed dict with list operations passes through unchanged.""" + ops = [{"op": "append_block", "section_id": "s1", "text": "hello"}] + raw = {"operations": ops} + + result, changed = _normalize_dict_operations(raw) + + assert changed is False + assert result is raw # Identity preserved + + def test_non_json_operations_string_logged_and_unchanged(self, caplog): + """Non-JSON operations string logs warning and returns unchanged.""" + import logging + + raw = {"operations": "not json"} + + with caplog.at_level( + logging.WARNING, logger="codespy.agents.memory.cerebral.delta_ops_compat" + ): + result, changed = _normalize_dict_operations(raw) + + assert changed is False + assert result is raw # Identity preserved + assert "non-JSON string" in caplog.text + + def test_no_operations_key(self): + """Dict without operations key passes through unchanged.""" + raw = {"other_key": "value"} + + result, changed = _normalize_dict_operations(raw) + + assert changed is False + assert result is raw + + def test_operations_none(self): + """Dict with operations=None passes through unchanged.""" + raw = {"operations": None} + + result, changed = _normalize_dict_operations(raw) + + assert changed is False + assert result["operations"] is None + + +class TestDecodeOperationStrings: + """Tests for _decode_operation_strings.""" + + def test_decode_json_strings_in_list(self): + """JSON strings in list are decoded to dicts.""" + op1 = {"op": "append_block", "section_id": "s1", "text": "hello"} + op2 = {"op": "replace_block", "section_id": "s1", "block_id": "b1", "text": "world"} + ops = [json.dumps(op1), json.dumps(op2)] + + result = _decode_operation_strings(ops) + + assert result == [op1, op2] + + def test_mixed_list_partially_decoded(self): + """Mixed list with some JSON strings and some dicts.""" + op1 = {"op": "append_block", "section_id": "s1", "text": "hello"} + op2 = {"op": "replace_block", "section_id": "s1", "block_id": "b1", "text": "world"} + ops = [json.dumps(op1), op2, "not json"] + + result = _decode_operation_strings(ops) + + assert result == [op1, op2, "not json"] + + def test_no_json_strings_identity_preserved(self): + """List without JSON strings returns same list (identity preserved).""" + ops = [{"op": "append_block"}, {"op": "replace_block"}] + + result = _decode_operation_strings(ops) + + assert result is ops # Identity preserved + + +class TestNormalize: + """Tests for _normalize.""" + + def test_string_input_with_encoded_operations(self): + """String input with encoded operations is parsed and normalized.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + # The string contains JSON where operations is a JSON-encoded string + text = json.dumps({"operations": json.dumps([op])}) + + result, changed = _normalize(text) + + assert changed is True + assert result["operations"] == [op] + + def test_string_input_well_formed(self): + """String input with well-formed operations is parsed and unchanged.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + text = json.dumps({"operations": [op]}) + + result, changed = _normalize(text) + + # changed is False because no actual decoding was needed (operations was already a list) + assert changed is False + assert result == {"operations": [op]} + + def test_dict_input(self): + """Dict input is normalized.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + raw = {"operations": json.dumps([op])} + + result, changed = _normalize(raw) + + assert changed is True + assert result["operations"] == [op] + + def test_list_input_unchanged(self): + """List input passes through unchanged.""" + ops = [{"op": "append_block"}] + + result, changed = _normalize(ops) + + assert changed is False + assert result is ops + + def test_invalid_json_string_unchanged(self): + """Invalid JSON string returns unchanged.""" + text = "not valid json" + + result, changed = _normalize(text) + + assert changed is False + assert result == text + + +class TestInstallDeltaOpsCoercion: + """Tests for install_delta_ops_coercion.""" + + def setup_method(self): + """Reset delta_ops module before each test.""" + # Store the original function + self._original = delta_ops.parse_delta_operation_list + # Ensure no coercion is installed - try to unwrap if needed + if hasattr(self._original, "_codespy_delta_coercion"): + # Get the original function from our wrapper + # The wrapper stores the original in its closure + if hasattr(self._original, "__wrapped__"): + delta_ops.parse_delta_operation_list = self._original.__wrapped__ + else: + # If __wrapped__ is not available, we need to get the original + # from the wrapper's closure + closure = getattr(self._original, "__closure__", None) + if closure: + delta_ops.parse_delta_operation_list = closure[0].cell_contents + + def teardown_method(self): + """Restore original function after each test.""" + delta_ops.parse_delta_operation_list = self._original + + def test_install_wraps_function(self): + """install_delta_ops_coercion wraps parse_delta_operation_list.""" + # Get the current function + original = delta_ops.parse_delta_operation_list + + # Install the coercion + install_delta_ops_coercion() + + # Should have been wrapped + assert delta_ops.parse_delta_operation_list is not original + assert hasattr(delta_ops.parse_delta_operation_list, "_codespy_delta_coercion") + + def test_idempotent_install(self): + """Multiple installs have no additional effect.""" + # Install once + install_delta_ops_coercion() + wrapped = delta_ops.parse_delta_operation_list + + # Install again + install_delta_ops_coercion() + wrapped2 = delta_ops.parse_delta_operation_list + + # Should be the same wrapper + assert wrapped is wrapped2 + + def test_wrapper_handles_encoded_operations(self): + """The wrapper correctly handles JSON-encoded operations.""" + # Install the coercion + install_delta_ops_coercion() + + # Test with encoded operations + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + raw = {"operations": json.dumps([op])} + + # This would previously raise TypeError + result = delta_ops.parse_delta_operation_list(raw) + + assert len(result.operations) == 1 + assert result.operations[0].op == "append_block" + + def test_wrapper_passes_well_formed_unchanged(self): + """The wrapper passes well-formed payloads unchanged.""" + # Install the coercion + install_delta_ops_coercion() + + # Test with well-formed operations + from hindsight_api.engine.reflect.delta_ops import AppendBlockOp + + ops = [AppendBlockOp(section_id="s1", text="hello")] + raw = {"operations": ops} + + result = delta_ops.parse_delta_operation_list(raw) + + assert len(result.operations) == 1 + assert result.operations[0].section_id == "s1" + + +class TestRealWorldScenarios: + """Tests simulating real-world error scenarios.""" + + def setup_method(self): + """Store original function before test.""" + self._original = delta_ops.parse_delta_operation_list + + def teardown_method(self): + """Restore original function after test.""" + delta_ops.parse_delta_operation_list = self._original + + def test_nemotron_double_encoding(self): + """Simulate the Nemotron double-encoding issue that caused the original bug.""" + # Install the coercion + install_delta_ops_coercion() + + # Simulate what Nemotron returns: operations is a JSON-encoded string + actual_ops = [ + {"op": "append_block", "section_id": "overview", "text": "Test content"}, + {"op": "add_section", "heading": "New Section", "blocks": ["Block content"]}, + ] + nemotron_response = {"operations": json.dumps(actual_ops)} + + # This was the original failure: TypeError: operations must be a list, got + # With the shim, it should work + result = delta_ops.parse_delta_operation_list(nemotron_response) + + assert len(result.operations) == 2 + assert result.operations[0].op == "append_block" + assert result.operations[1].op == "add_section" + + def test_nested_operations_wrapping(self): + """Test handling of double-wrapped operations dict.""" + # Install the coercion + install_delta_ops_coercion() + + # Sometimes the model wraps the whole thing in another operations key + inner = {"operations": [{"op": "append_block", "section_id": "s1", "text": "hello"}]} + raw = {"operations": json.dumps(inner)} + + result = delta_ops.parse_delta_operation_list(raw) + + assert len(result.operations) == 1 + assert result.operations[0].op == "append_block" + + def test_text_input_with_encoded_operations(self): + """Test handling of text input containing encoded operations.""" + # Install the coercion + install_delta_ops_coercion() + + # Text input that contains JSON with string-encoded operations + ops = [{"op": "replace_block", "section_id": "s1", "block_id": "b1", "text": "new text"}] + text = json.dumps({"operations": json.dumps(ops)}) + + result = delta_ops.parse_delta_operation_list(text) + + assert len(result.operations) == 1 + assert result.operations[0].op == "replace_block" + + +class TestCallerDictNotMutated: + """Tests ensuring the caller's dict is not mutated.""" + + def test_caller_dict_not_mutated(self): + """The caller's dict should never be mutated.""" + op = {"op": "append_block", "section_id": "s1", "text": "hello"} + original_raw = {"operations": json.dumps([op]), "other_key": "value"} + original_copy = json.loads(json.dumps(original_raw)) + + result, changed = _normalize_dict_operations(original_raw) + + assert changed is True + # Original dict should be unchanged + assert original_raw == original_copy + # Result should be a new dict + assert result is not original_raw + assert result["operations"] == [op] + assert result["other_key"] == "value" diff --git a/tests/test_cerebral_orphans.py b/tests/test_cerebral_orphans.py new file mode 100644 index 0000000..053d66d --- /dev/null +++ b/tests/test_cerebral_orphans.py @@ -0,0 +1,157 @@ +"""Tests for the orphaned async operation repair module.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +pytest.importorskip("hindsight_api") + +from codespy.agents.memory.cerebral.orphans import ( + _ORPHAN_OPERATION_TYPES, + repair_orphaned_operations, +) + + +class TestRepairOrphanedOperations: + """Tests for repair_orphaned_operations function.""" + + @pytest.fixture + def mock_engine(self): + """Create a mock MemoryEngine with a mock _backend.""" + engine = MagicMock() + engine._backend = MagicMock() + return engine + + @pytest.fixture + def mock_conn(self): + """Create a mock async connection.""" + conn = AsyncMock() + return conn + + @pytest.mark.asyncio + async def test_marks_pending_operations_as_failed(self, mock_engine, mock_conn): + """Test that pending operations older than threshold are marked failed.""" + # Set up mock rows to return (simulating 2 orphaned operations) + mock_rows = [ + {"operation_id": "op-1", "operation_type": "refresh_mental_model"}, + {"operation_id": "op-2", "operation_type": "consolidation"}, + ] + mock_conn.fetch.return_value = mock_rows + + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.return_value.__aenter__ = AsyncMock(return_value=mock_conn) + mock_acquire.return_value.__aexit__ = AsyncMock(return_value=None) + + result = await repair_orphaned_operations( + mock_engine, "semantic", "test-bank", older_than_s=3600 + ) + + assert result == 2 + + # Verify the SQL was called with correct parameters + call_args = mock_conn.fetch.call_args + assert call_args[0][1] == "test-bank" # bank_id + assert "orphaned: left pending by inline SyncTaskBackend" in call_args[0][2] + assert call_args[0][3] == _ORPHAN_OPERATION_TYPES + assert call_args[0][4] == 3600 # older_than_s + + @pytest.mark.asyncio + async def test_no_orphans_found(self, mock_engine, mock_conn): + """Test when no orphaned operations are found.""" + mock_conn.fetch.return_value = [] + + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.return_value.__aenter__ = AsyncMock(return_value=mock_conn) + mock_acquire.return_value.__aexit__ = AsyncMock(return_value=None) + + result = await repair_orphaned_operations( + mock_engine, "semantic", "test-bank" + ) + + assert result == 0 + + @pytest.mark.asyncio + async def test_db_error_returns_zero(self, mock_engine): + """Test that database errors are caught and return 0.""" + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.side_effect = Exception("connection failed") + + result = await repair_orphaned_operations( + mock_engine, "semantic", "test-bank" + ) + + assert result == 0 + + @pytest.mark.asyncio + async def test_uses_default_age_threshold(self, mock_engine, mock_conn): + """Test that default age threshold (3600s) is used when not specified.""" + mock_conn.fetch.return_value = [] + + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.return_value.__aenter__ = AsyncMock(return_value=mock_conn) + mock_acquire.return_value.__aexit__ = AsyncMock(return_value=None) + + await repair_orphaned_operations(mock_engine, "semantic", "test-bank") + + call_args = mock_conn.fetch.call_args + assert call_args[0][4] == 3600 # Default older_than_s + + @pytest.mark.asyncio + async def test_custom_age_threshold(self, mock_engine, mock_conn): + """Test that custom age threshold is passed through.""" + mock_conn.fetch.return_value = [] + + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.return_value.__aenter__ = AsyncMock(return_value=mock_conn) + mock_acquire.return_value.__aexit__ = AsyncMock(return_value=None) + + await repair_orphaned_operations( + mock_engine, "semantic", "test-bank", older_than_s=7200 + ) + + call_args = mock_conn.fetch.call_args + assert call_args[0][4] == 7200 + + @pytest.mark.asyncio + async def test_only_affects_specified_bank(self, mock_engine, mock_conn): + """Test that only operations for the specified bank are affected.""" + mock_conn.fetch.return_value = [] + + with patch( + "codespy.agents.memory.cerebral.orphans.acquire_with_retry" + ) as mock_acquire: + mock_acquire.return_value.__aenter__ = AsyncMock(return_value=mock_conn) + mock_acquire.return_value.__aexit__ = AsyncMock(return_value=None) + + await repair_orphaned_operations( + mock_engine, "semantic", "specific-bank" + ) + + call_args = mock_conn.fetch.call_args + assert call_args[0][1] == "specific-bank" + + +class TestOrphanOperationTypes: + """Tests for the operation types constant.""" + + def test_includes_refresh_mental_model(self): + """Test that refresh_mental_model is included.""" + assert "refresh_mental_model" in _ORPHAN_OPERATION_TYPES + + def test_includes_consolidation(self): + """Test that consolidation is included.""" + assert "consolidation" in _ORPHAN_OPERATION_TYPES + + def test_has_exactly_two_types(self): + """Test that there are exactly two operation types.""" + assert len(_ORPHAN_OPERATION_TYPES) == 2 diff --git a/tests/test_cerebral_routines.py b/tests/test_cerebral_routines.py new file mode 100644 index 0000000..53b4de5 --- /dev/null +++ b/tests/test_cerebral_routines.py @@ -0,0 +1,324 @@ +"""Unit tests for cerebral.routines module. + +These tests verify that the maintenance routine repair works correctly +without needing a live database. The tests mock the migration file and +database connection where necessary. +""" + +from __future__ import annotations + +import os +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +import pytest + + +class TestCollectRoutineSql: + """Tests for collect_routine_sql function.""" + + @pytest.fixture(autouse=True) + def reset_env(self, monkeypatch): + """Ensure HINDSIGHT_API_DATABASE_SCHEMA matches the test schema.""" + # Import the module fresh for each test + monkeypatch.setenv("HINDSIGHT_API_DATABASE_SCHEMA", "semantic") + yield + + def test_returns_all_four_routines(self): + """Test that collect_routine_sql returns all four expected routines.""" + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + expected_routines = { + "banks_needing_consolidation", + "schemas_with_expired_rows", + "mental_models_with_cron", + "schemas_with_expired_operations", + } + + assert set(result.keys()) == expected_routines + + def test_sql_contains_schema_qualification(self): + """Test that each SQL statement contains the schema qualification.""" + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + for name, sql in result.items(): + expected = f'"semantic".{name}' + assert expected in sql, f"SQL for {name} missing schema qualification" + + def test_sql_contains_create_or_replace(self): + """Test that each SQL statement contains CREATE OR REPLACE.""" + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + for name, sql in result.items(): + assert "CREATE OR REPLACE FUNCTION" in sql, f"SQL for {name} missing CREATE OR REPLACE" + + def test_no_public_in_sql(self): + """Test that SQL does not contain 'public' when semantic schema is requested.""" + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + for name, sql in result.items(): + # Should not have "public" qualified in the function definition + assert '"public".' not in sql, f"SQL for {name} incorrectly contains 'public'" + + +class TestExternallyOwnedRoutines: + """Tests for handling of externally owned routines.""" + + def test_externally_owned_routine_skipped(self, monkeypatch): + """Test that externally owned routines are skipped.""" + monkeypatch.setenv("HINDSIGHT_API_DATABASE_SCHEMA", "semantic") + monkeypatch.setenv( + "HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES", + "mental_models_with_cron" + ) + + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + # mental_models_with_cron should be missing + assert "mental_models_with_cron" not in result + # The others should be present + assert "banks_needing_consolidation" in result + assert "schemas_with_expired_rows" in result + assert "schemas_with_expired_operations" in result + + def test_multiple_externally_owned_routines(self, monkeypatch): + """Test that multiple externally owned routines are all skipped.""" + monkeypatch.setenv("HINDSIGHT_API_DATABASE_SCHEMA", "semantic") + monkeypatch.setenv( + "HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES", + "mental_models_with_cron,banks_needing_consolidation" + ) + + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + assert "mental_models_with_cron" not in result + assert "banks_needing_consolidation" not in result + assert "schemas_with_expired_rows" in result + assert "schemas_with_expired_operations" in result + + def test_whitespace_handling_in_env_var(self, monkeypatch): + """Test that whitespace around routine names is handled correctly.""" + monkeypatch.setenv("HINDSIGHT_API_DATABASE_SCHEMA", "semantic") + monkeypatch.setenv( + "HINDSIGHT_API_EXTERNALLY_OWNED_ROUTINES", + " mental_models_with_cron , banks_needing_consolidation " + ) + + from codespy.agents.memory.cerebral import routines as routines_module + + result = routines_module.collect_routine_sql("semantic") + + assert "mental_models_with_cron" not in result + assert "banks_needing_consolidation" not in result + + +class TestCollectRoutineSqlErrors: + """Tests for error handling in collect_routine_sql.""" + + def test_migration_file_not_found(self): + """Test that RuntimeError is raised when migration file is missing.""" + from codespy.agents.memory.cerebral import routines as routines_module + + # Patch _get_migration_path to return None + with patch.object(routines_module, "_get_migration_path", return_value=None): + with pytest.raises(RuntimeError) as exc_info: + routines_module.collect_routine_sql("semantic") + + assert "c8b4e2a71f95" in str(exc_info.value) + assert "not found" in str(exc_info.value) + + +class TestEnsureMaintenanceRoutines: + """Tests for ensure_maintenance_routines function.""" + + @pytest.fixture + def mock_engine(self): + """Create a mock MemoryEngine with _backend.""" + engine = MagicMock() + engine._backend = MagicMock() + return engine + + @pytest.fixture + def mock_sql_map(self): + """Create a mock SQL map with all four routines.""" + return { + "banks_needing_consolidation": "CREATE OR REPLACE FUNCTION ...", + "schemas_with_expired_rows": "CREATE OR REPLACE FUNCTION ...", + "mental_models_with_cron": "CREATE OR REPLACE FUNCTION ...", + "schemas_with_expired_operations": "CREATE OR REPLACE FUNCTION ...", + } + + @pytest.mark.asyncio + async def test_all_present_returns_empty_list(self, mock_engine, mock_sql_map): + """Test that when all routines are present, returns empty list.""" + from codespy.agents.memory.cerebral import routines as routines_module + + # Create a proper async mock context manager + async def mock_fetch(*args, **kwargs): + return [ + {"proname": "banks_needing_consolidation"}, + {"proname": "schemas_with_expired_rows"}, + {"proname": "mental_models_with_cron"}, + {"proname": "schemas_with_expired_operations"}, + ] + + mock_conn = MagicMock() + mock_conn.fetch = AsyncMock(side_effect=mock_fetch) + + # Mock acquire_with_retry to return mock_conn via async context manager + @MagicMock + async def mock_acquire_cm(*args, **kwargs): + class CtxManager: + async def __aenter__(self): + return mock_conn + async def __aexit__(self, *args): + return None + return CtxManager() + + # Create a proper async context manager + class AsyncContextManager: + async def __aenter__(self): + return mock_conn + async def __aexit__(self, *args): + return None + + with patch.object(routines_module, "collect_routine_sql", return_value=mock_sql_map): + with patch("hindsight_api.engine.db_utils.acquire_with_retry", return_value=AsyncContextManager()): + result = await routines_module.ensure_maintenance_routines( + mock_engine, "semantic" + ) + + assert result == [] + + @pytest.mark.asyncio + async def test_missing_routines_get_installed(self, mock_engine, mock_sql_map): + """Test that missing routines are installed.""" + from codespy.agents.memory.cerebral import routines as routines_module + + # Track which SQL statements were executed + executed_sql = [] + + async def mock_fetch(*args, **kwargs): + # Only two routines present + return [ + {"proname": "banks_needing_consolidation"}, + {"proname": "schemas_with_expired_rows"}, + ] + + async def mock_execute(sql): + executed_sql.append(sql) + + mock_conn = MagicMock() + mock_conn.fetch = AsyncMock(side_effect=mock_fetch) + mock_conn.execute = AsyncMock(side_effect=mock_execute) + + # Create a proper async context manager + class AsyncContextManager: + async def __aenter__(self): + return mock_conn + async def __aexit__(self, *args): + return None + + class MockTransaction: + async def __aenter__(self): + return None + async def __aexit__(self, *args): + return None + + mock_conn.transaction = MagicMock(return_value=MockTransaction()) + + with patch.object(routines_module, "collect_routine_sql", return_value=mock_sql_map): + with patch("hindsight_api.engine.db_utils.acquire_with_retry", return_value=AsyncContextManager()): + result = await routines_module.ensure_maintenance_routines( + mock_engine, "semantic" + ) + + # Should have installed the two missing routines + assert len(result) == 2 + assert "mental_models_with_cron" in result + assert "schemas_with_expired_operations" in result + + @pytest.mark.asyncio + async def test_collector_failure_returns_empty_list(self, mock_engine): + """Test that if collect_routine_sql fails, we return empty list and log warning.""" + from codespy.agents.memory.cerebral import routines as routines_module + + with patch.object( + routines_module, "collect_routine_sql", side_effect=RuntimeError("Migration not found") + ): + result = await routines_module.ensure_maintenance_routines(mock_engine, "semantic") + assert result == [] + + @pytest.mark.asyncio + async def test_db_check_failure_returns_empty_list(self, mock_engine, mock_sql_map): + """Test that if DB check fails, we return empty list and log warning.""" + from codespy.agents.memory.cerebral import routines as routines_module + + # Create a proper async context manager that raises on enter + class AsyncContextManager: + async def __aenter__(self): + raise Exception("Connection failed") + async def __aexit__(self, *args): + return None + + with patch.object(routines_module, "collect_routine_sql", return_value=mock_sql_map): + with patch("hindsight_api.engine.db_utils.acquire_with_retry", return_value=AsyncContextManager()): + result = await routines_module.ensure_maintenance_routines(mock_engine, "semantic") + assert result == [] + + +class TestGetMigrationPath: + """Tests for _get_migration_path helper.""" + + def test_finds_migration_file(self): + """Test that the migration file is found.""" + from codespy.agents.memory.cerebral import routines as routines_module + + path = routines_module._get_migration_path() + assert path is not None + assert path.exists() + assert "c8b4e2a71f95" in path.name + + def test_returns_none_when_hindsight_not_importable(self): + """Test that None is returned if hindsight_api is not available.""" + from codespy.agents.memory.cerebral import routines as routines_module + + with patch.object(routines_module, "_get_migration_path", return_value=None): + # Test that our patch works + path = routines_module._get_migration_path() + assert path is None + + +class TestRoutineNamesConstant: + """Tests for the ROUTINE_NAMES constant.""" + + def test_contains_all_four_routines(self): + """Test that ROUTINE_NAMES contains all four expected routine names.""" + from codespy.agents.memory.cerebral import routines as routines_module + + expected = [ + "banks_needing_consolidation", + "schemas_with_expired_rows", + "mental_models_with_cron", + "schemas_with_expired_operations", + ] + assert routines_module.ROUTINE_NAMES == expected + + def test_revision_id_is_set(self): + """Test that REVISION_ID is set to expected value.""" + from codespy.agents.memory.cerebral import routines as routines_module + + assert routines_module.REVISION_ID == "c8b4e2a71f95" diff --git a/tests/test_cerebral_structured_output.py b/tests/test_cerebral_structured_output.py new file mode 100644 index 0000000..993c16f --- /dev/null +++ b/tests/test_cerebral_structured_output.py @@ -0,0 +1,636 @@ +"""Tests for Cerebral structured output support. + +Covers: +- Native structured output registration +- JSON schema sanitization for Bedrock +- Two-step structured output wrapper +""" + +from __future__ import annotations + +import copy +from typing import Any +from unittest.mock import MagicMock, Mock, patch + +import pytest + +# Skip all tests if litellm is not available +try: + import litellm + + HAS_LITELLM = True +except ImportError: + HAS_LITELLM = False + +pytestmark = pytest.mark.skipif(not HAS_LITELLM, reason="litellm not installed") + + +class TestRegisterNativeStructuredOutput: + """Tests for register_native_structured_output_overrides.""" + + def test_registration_makes_super_capable(self): + """After registration, Super should report native structured output support.""" + from litellm.utils import supports_native_structured_output + from codespy.agents.memory.cerebral.structured_output import ( + register_native_structured_output_overrides, + ) + + register_native_structured_output_overrides() + + # Check the bare key + result = supports_native_structured_output( + "nvidia.nemotron-super-3-120b", "bedrock_converse" + ) + assert result is True + + def test_registration_is_idempotent(self): + """Multiple registrations should not cause errors.""" + from litellm.utils import supports_native_structured_output + from codespy.agents.memory.cerebral.structured_output import ( + register_native_structured_output_overrides, + ) + + # Register twice + register_native_structured_output_overrides() + register_native_structured_output_overrides() + + # Should still work + result = supports_native_structured_output( + "nvidia.nemotron-super-3-120b", "bedrock_converse" + ) + assert result is True + + def test_pricing_preserved_after_registration(self): + """Model pricing should be preserved after registration.""" + from codespy.agents.memory.cerebral.structured_output import ( + register_native_structured_output_overrides, + ) + + register_native_structured_output_overrides() + + # Get model info + info = litellm.get_model_info("nvidia.nemotron-super-3-120b", "bedrock_converse") + # Should have pricing info + assert info is not None + + +class TestSanitizeBedrockJsonSchema: + """Tests for sanitize_bedrock_json_schema.""" + + def test_strips_minimum_maximum(self): + """Should strip minimum/maximum keywords.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + } + }, + } + + result = sanitize_bedrock_json_schema(schema) + + assert "minimum" not in result["properties"]["level"] + assert "maximum" not in result["properties"]["level"] + assert result["properties"]["level"]["type"] == "integer" + + def test_strips_string_constraints(self): + """Should strip minLength/maxLength/pattern.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-z]+$", + } + }, + } + + result = sanitize_bedrock_json_schema(schema) + + assert "minLength" not in result["properties"]["name"] + assert "maxLength" not in result["properties"]["name"] + assert "pattern" not in result["properties"]["name"] + assert result["properties"]["name"]["type"] == "string" + + def test_strips_max_items(self): + """Should strip maxItems from arrays.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": {"type": "string"}, + "maxItems": 100, + } + }, + } + + result = sanitize_bedrock_json_schema(schema) + + assert "maxItems" not in result["properties"]["items"] + + def test_strips_min_items_gt_1(self): + """Should strip minItems only when value > 1.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + # minItems: 2 should be stripped + schema_gt1 = { + "type": "array", + "minItems": 2, + "items": {"type": "string"}, + } + result = sanitize_bedrock_json_schema(schema_gt1) + assert "minItems" not in result + + # minItems: 0 should be kept + schema_0 = { + "type": "array", + "minItems": 0, + "items": {"type": "string"}, + } + result = sanitize_bedrock_json_schema(schema_0) + assert result.get("minItems") == 0 + + # minItems: 1 should be kept + schema_1 = { + "type": "array", + "minItems": 1, + "items": {"type": "string"}, + } + result = sanitize_bedrock_json_schema(schema_1) + assert result.get("minItems") == 1 + + def test_forces_additional_properties_false(self): + """Should force additionalProperties to false.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "additionalProperties": {"type": "string"}, + } + + result = sanitize_bedrock_json_schema(schema) + + assert result["additionalProperties"] is False + + def test_keeps_additional_properties_false(self): + """Should keep additionalProperties: false as-is.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "additionalProperties": False, + } + + result = sanitize_bedrock_json_schema(schema) + + assert result["additionalProperties"] is False + + def test_strips_disallowed_format_values(self): + """Should strip format values outside allowed set.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "email": {"type": "string", "format": "email"}, + "date": {"type": "string", "format": "date-time"}, + "custom": {"type": "string", "format": "custom-format"}, + }, + } + + result = sanitize_bedrock_json_schema(schema) + + # Allowed formats should be kept + assert result["properties"]["email"]["format"] == "email" + assert result["properties"]["date"]["format"] == "date-time" + # Disallowed format should be stripped + assert "format" not in result["properties"]["custom"] + + def test_preserves_property_names(self): + """Should not strip keys under properties (they're names, not keywords).""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "minimum": {"type": "string"}, + "maximum": {"type": "string"}, + }, + } + + result = sanitize_bedrock_json_schema(schema) + + # Property names should be preserved + assert "minimum" in result["properties"] + assert "maximum" in result["properties"] + + def test_does_not_mutate_input(self): + """Should not mutate the input schema.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "type": "object", + "properties": { + "level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + } + }, + } + original = copy.deepcopy(schema) + + sanitize_bedrock_json_schema(schema) + + assert schema == original + + def test_recurses_through_refs(self): + """Should recurse through $defs/definitions.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "$defs": { + "Level": { + "type": "integer", + "minimum": 1, + "maximum": 6, + } + }, + "properties": { + "level": {"$ref": "#/$defs/Level"} + }, + } + + result = sanitize_bedrock_json_schema(schema) + + # Should strip from $defs + assert "minimum" not in result["$defs"]["Level"] + assert "maximum" not in result["$defs"]["Level"] + + def test_recurses_through_combiners(self): + """Should recurse through anyOf/allOf/oneOf.""" + from codespy.agents.memory.cerebral.structured_output import ( + sanitize_bedrock_json_schema, + ) + + schema = { + "anyOf": [ + {"type": "integer", "minimum": 0}, + {"type": "string", "maxLength": 10}, + ] + } + + result = sanitize_bedrock_json_schema(schema) + + assert "minimum" not in result["anyOf"][0] + assert "maxLength" not in result["anyOf"][1] + + +class TestEnforcesSchema: + """Tests for enforces_schema function.""" + + def test_returns_true_for_native_supported(self): + """Should return True for models with native structured output.""" + from codespy.agents.memory.cerebral.structured_output import ( + enforces_schema, + register_native_structured_output_overrides, + ) + + register_native_structured_output_overrides() + + result = enforces_schema("bedrock/converse/nvidia.nemotron-super-3-120b") + # Result should be True (or None if litellm doesn't support the function) + assert result is True or result is None + + def test_returns_false_for_tool_fallback_models(self): + """Should return False for models using tool fallback.""" + from codespy.agents.memory.cerebral.structured_output import ( + enforces_schema, + ) + + # A model without native support (unknown/unmapped) + result = enforces_schema("bedrock/converse/unknown-model") + # Should be None or False depending on litellm + assert result in (None, False) + + def test_returns_none_for_unmapped_models(self): + """Should return None for unmapped models.""" + from codespy.agents.memory.cerebral.structured_output import ( + enforces_schema, + ) + + result = enforces_schema("unmapped-provider/unknown-model") + # Result is None if function not found, or False/None if model not found + assert result is None or result is False + + +class TestInstallBedrockSchemaSanitizer: + """Tests for install_bedrock_schema_sanitizer.""" + + def test_is_idempotent(self): + """Should be safe to call multiple times.""" + from codespy.agents.memory.cerebral.structured_output import ( + install_bedrock_schema_sanitizer, + ) + + # Should not raise + install_bedrock_schema_sanitizer() + install_bedrock_schema_sanitizer() + + def test_sanitizer_strips_keywords(self): + """Installed sanitizer should strip disallowed keywords.""" + from codespy.agents.memory.cerebral.structured_output import ( + install_bedrock_schema_sanitizer, + sanitize_bedrock_json_schema, + ) + + install_bedrock_schema_sanitizer() + + # The sanitizer wraps _create_output_config_for_response_format + # We can verify it by checking the underlying function is wrapped + from litellm.llms.bedrock.chat.converse_transformation import ( + AmazonConverseConfig, + ) + + method = getattr( + AmazonConverseConfig, "_create_output_config_for_response_format" + ) + # Check marker is set + assert getattr(method, "_codespy_schema_sanitizer", False) + + +class TestTwoStepStructuredLLM: + """Tests for TwoStepStructuredLLM class.""" + + def test_getattr_forwards_to_main(self): + """Attribute access should forward to main LLM.""" + from codespy.agents.memory.cerebral.structured_output import ( + TwoStepStructuredLLM, + ) + + main = MagicMock() + main.model = "main-model" + main.provider = "bedrock" + + wrapper = TwoStepStructuredLLM(main, MagicMock()) + + assert wrapper.model == "main-model" + assert wrapper.provider == "bedrock" + + @pytest.mark.asyncio + async def test_passthrough_without_response_format(self): + """Should pass through to main when no response_format.""" + from unittest.mock import AsyncMock + from codespy.agents.memory.cerebral.structured_output import ( + TwoStepStructuredLLM, + ) + + main = MagicMock() + main.call = AsyncMock(return_value=MagicMock()) + extraction = MagicMock() + + wrapper = TwoStepStructuredLLM(main, extraction) + await wrapper.call([{"role": "user", "content": "test"}], response_format=None) + + main.call.assert_called_once() + extraction.call.assert_not_called() + + @pytest.mark.asyncio + async def test_builds_step1_messages_with_schema(self): + """Should append schema instruction to system message.""" + from unittest.mock import AsyncMock, patch + from pydantic import BaseModel + from codespy.agents.memory.cerebral.structured_output import ( + TwoStepStructuredLLM, + ) + + # Create a mock result with content (not text) + mock_result = MagicMock() + mock_result.content = '{"key": "value"}' + mock_result.usage = MagicMock() + mock_result.usage.__add__ = MagicMock(return_value=MagicMock()) + mock_result.model_copy = MagicMock(return_value=mock_result) + + main = MagicMock() + main.call = AsyncMock(return_value=mock_result) + + extraction = MagicMock() + extraction.call = AsyncMock(return_value=mock_result) + + wrapper = TwoStepStructuredLLM(main, extraction) + + # Create a pydantic model for response_format + class MockModel(BaseModel): + key: str + + messages = [{"role": "system", "content": "You are helpful"}] + await wrapper.call(messages, response_format=MockModel, scope="test") + + # Check main was called with modified messages + call_args = main.call.call_args + modified_messages = call_args[1].get("messages", call_args[0][0]) + + # System message should contain schema + assert len(modified_messages) == 1 + assert "JSON schema" in modified_messages[0]["content"] + + +class TestInstallTwoStepStructuredOutput: + """Tests for install_two_step_structured_output.""" + + def test_skips_multi_llm_provider(self): + """Should skip MultiLLMProvider implementations.""" + from codespy.agents.memory.cerebral.structured_output import ( + enforces_schema, + install_two_step_structured_output, + ) + + # Create mock engine with MultiLLMProvider + engine = MagicMock() + engine._llm_config = MagicMock() + + multi_provider = MagicMock() + multi_provider.__class__.__name__ = "MultiLLMProvider" + engine._llm_config._provider_impl = multi_provider + + extraction_impl = MagicMock() + + install_two_step_structured_output(engine, extraction_impl) + + # Multi provider should not be wrapped + assert engine._llm_config._provider_impl is multi_provider + + def test_skips_when_main_enforces(self): + """Should skip when main model already enforces schema.""" + from codespy.agents.memory.cerebral.structured_output import ( + install_two_step_structured_output, + ) + + # Mock enforces_schema to return True for main + with patch( + "codespy.agents.memory.cerebral.structured_output.enforces_schema" + ) as mock_enforces: + mock_enforces.return_value = True + + engine = MagicMock() + llm = MagicMock() + llm.model = "bedrock/converse/nvidia.nemotron-super-3-120b" + engine._llm_config = MagicMock() + engine._llm_config._provider_impl = llm + + extraction_impl = MagicMock() + extraction_impl.model = "nvidia.nemotron-nano-3-30b" + + install_two_step_structured_output(engine, extraction_impl) + + # Should not be wrapped + assert engine._llm_config._provider_impl is llm + + @pytest.mark.skip(reason="Cannot easily mock isinstance with real LiteLLMLLM class") + def test_wraps_when_main_does_not_enforce(self): + """Should wrap when main doesn't enforce but extraction does.""" + from codespy.agents.memory.cerebral.structured_output import ( + TwoStepStructuredLLM, + install_two_step_structured_output, + ) + + # Mock enforces_schema: main=False, extraction=True + def enforces_side_effect(model): + if "super" in model: + return False + if "nano" in model: + return True + return None + + extraction_impl = MagicMock() + extraction_impl.model = "nvidia.nemotron-nano-3-30b" + + with patch( + "codespy.agents.memory.cerebral.structured_output.enforces_schema" + ) as mock_enforces: + mock_enforces.side_effect = enforces_side_effect + + engine = MagicMock() + llm = MagicMock() + llm.model = "bedrock/converse/nvidia.nemotron-super-3-120b" + engine._llm_config = MagicMock() + engine._llm_config._provider_impl = llm + engine._retain_llm_config = None + engine._consolidation_llm_config = None + engine._reflect_llm_config = None + engine._mental_model_refresh_llm_override = None + + install_two_step_structured_output(engine, extraction_impl) + + # Should be wrapped + assert isinstance(engine._llm_config._provider_impl, TwoStepStructuredLLM) + + @pytest.mark.skip(reason="Cannot easily mock isinstance with real LiteLLMLLM class") + def test_wraps_only_once_per_config(self): + """Should wrap shared configs only once.""" + from codespy.agents.memory.cerebral.structured_output import ( + TwoStepStructuredLLM, + install_two_step_structured_output, + ) + + def enforces_side_effect(model): + if "super" in model: + return False + if "nano" in model: + return True + return None + + extraction_impl = MagicMock() + extraction_impl.model = "nvidia.nemotron-nano-3-30b" + + with patch( + "codespy.agents.memory.cerebral.structured_output.enforces_schema" + ) as mock_enforces: + mock_enforces.side_effect = enforces_side_effect + + shared_config = MagicMock() + llm = MagicMock() + llm.model = "bedrock/converse/nvidia.nemotron-super-3-120b" + shared_config._provider_impl = llm + + engine = MagicMock() + engine._llm_config = shared_config + engine._retain_llm_config = shared_config # Same config + engine._consolidation_llm_config = None + engine._reflect_llm_config = None + engine._mental_model_refresh_llm_override = None + + install_two_step_structured_output(engine, extraction_impl) + + # Both should point to the same wrapper + assert isinstance(engine._llm_config._provider_impl, TwoStepStructuredLLM) + assert isinstance(engine._retain_llm_config._provider_impl, TwoStepStructuredLLM) + # Should be the same wrapper instance + assert ( + engine._llm_config._provider_impl is engine._retain_llm_config._provider_impl + ) + + +class TestIntegration: + """Integration tests combining multiple features.""" + + def test_full_workflow_registration_then_sanitizer(self): + """Should be able to register overrides then install sanitizer.""" + from litellm.utils import supports_native_structured_output + from codespy.agents.memory.cerebral.structured_output import ( + install_bedrock_schema_sanitizer, + register_native_structured_output_overrides, + sanitize_bedrock_json_schema, + ) + + # Register first + register_native_structured_output_overrides() + + # Then install sanitizer + install_bedrock_schema_sanitizer() + + # Verify both work + result = supports_native_structured_output( + "nvidia.nemotron-super-3-120b", "bedrock_converse" + ) + assert result is True + + # Sanitizer should work + schema = { + "type": "object", + "properties": {"level": {"type": "integer", "minimum": 1, "maximum": 6}}, + } + result = sanitize_bedrock_json_schema(schema) + assert "minimum" not in result["properties"]["level"] diff --git a/tests/test_config.py b/tests/test_config.py index 89389f3..42f4253 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,7 @@ """Tests for Settings configuration. -Tests for the Settings class in codespy.config, covering top-level -boolean configuration fields. +Tests for the Settings class in codespy.config, covering the new +restructured configuration. """ import os @@ -16,42 +16,388 @@ 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 +class TestSettingsStructure: + """Tests for the restructured configuration.""" + + def test_llm_config_exists(self): + """Test that llm config exists and has expected fields.""" + settings = Settings() + assert hasattr(settings, "llm") + assert hasattr(settings.llm, "default_model") + assert hasattr(settings.llm, "retries") + assert hasattr(settings.llm, "timeout") + + def test_review_config_exists(self): + """Test that review config exists and has expected fields.""" + settings = Settings() + assert hasattr(settings, "review") + assert hasattr(settings.review, "min_confidence") + assert hasattr(settings.review, "output_format") + assert hasattr(settings.review, "output_stdout") + assert hasattr(settings.review, "output_git") + assert hasattr(settings.review, "cache_dir") + + def test_memory_config_exists(self): + """Test that memory config exists and has expected fields.""" + settings = Settings() + assert hasattr(settings, "memory") + assert hasattr(settings.memory, "enabled") + assert hasattr(settings.memory, "hippocampus") + assert hasattr(settings.memory, "cerebral") + + def test_signature_configs_exist(self): + """Test that signature configs exist in review section.""" + settings = Settings() + assert hasattr(settings.review, "code_review") + assert hasattr(settings.review, "doc") + assert hasattr(settings.review, "scope") + assert hasattr(settings.review, "supply_chain") + assert hasattr(settings.review, "summary") + assert hasattr(settings.review, "audit") + + def test_old_top_level_fields_removed(self): + """Test that old top-level fields are removed.""" + settings = Settings() + # These should no longer exist at top level + old_fields = [ + "default_model", + "extraction_model", + "default_max_iters", + "default_max_llm_calls", + "default_reasoning_effort", + "default_temperature", + "default_max_tokens", + "llm_retries", + "llm_timeout", + "rlm_fallback", + "signatures", + "min_confidence", + "output_format", + "output_stdout", + "output_git", + "cache_dir", + "excluded_directories", + "enable_prompt_caching", + "compact_patches", + ] + for field in old_fields: + assert not hasattr(settings, field), f"Old field '{field}' should not exist" + + def test_env_override_mapping(self, monkeypatch, tmp_path): + """Test that env vars are mapped correctly to new paths.""" + # Apply isolation: change to temp dir and use monkeypatch for env + monkeypatch.chdir(tmp_path) + + # DEFAULT_MODEL works (new name, no LLM_ prefix) + monkeypatch.setenv("DEFAULT_MODEL", "test-model") + settings = Settings() + assert settings.llm.default_model == "test-model" + monkeypatch.delenv("DEFAULT_MODEL") + + # LLM_DEFAULT_MODEL is ignored (old name no longer works) + monkeypatch.setenv("LLM_DEFAULT_MODEL", "ignored-model") + settings = Settings() + # Should use default, not the ignored env var + assert settings.llm.default_model != "ignored-model" + monkeypatch.delenv("LLM_DEFAULT_MODEL") + + # LLM_RETRIES still works (exception that keeps LLM_ prefix) + monkeypatch.setenv("LLM_RETRIES", "7") + settings = Settings() + assert settings.llm.retries == 7 + monkeypatch.delenv("LLM_RETRIES") + + +class TestReflectionModules: + """Tests for memory reflection modules and unit names.""" + + def test_reflection_modules_has_prefixed_names(self): + """REFLECTION_MODULES should use memory_* prefixed names.""" + from codespy.config_memory import REFLECTION_MODULES + + assert REFLECTION_MODULES == ("memory_distiller", "memory_cartographer") + + def test_reflection_module_config_helper(self): + """reflection_module_config removes prefix and returns config.""" + from codespy.config_memory import ( + HippocampusConfig, + reflection_module_config, + ) + + hippocampus = HippocampusConfig() + config = reflection_module_config(hippocampus, "memory_distiller") + assert config is hippocampus.distiller + + config = reflection_module_config(hippocampus, "memory_cartographer") + assert config is hippocampus.cartographer + + def test_get_llm_config_for_memory_distiller(self, monkeypatch, tmp_path): + """get_llm_config('memory_distiller') reads memory.hippocampus.distiller.""" + monkeypatch.chdir(tmp_path) + + # Create a config file with custom distiller model + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + hippocampus: + distiller: + model: anthropic/claude-3-haiku +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config("memory_distiller") + assert llm_config.model == "anthropic/claude-3-haiku" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_cartographer(self, monkeypatch, tmp_path): + """get_llm_config('memory_cartographer') reads memory.hippocampus.cartographer.""" + monkeypatch.chdir(tmp_path) + + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + hippocampus: + cartographer: + model: anthropic/claude-3-sonnet +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config("memory_cartographer") + assert llm_config.model == "anthropic/claude-3-sonnet" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_retain(self, monkeypatch, tmp_path): + """get_llm_config('memory_retain') reads memory.cerebral.retain with fallback.""" + monkeypatch.chdir(tmp_path) + + # Test with explicit retain model + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + cerebral: + retain: + model: anthropic/claude-3-opus +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config("memory_retain") + assert llm_config.model == "anthropic/claude-3-opus" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_retain_fallback(self, monkeypatch, tmp_path): + """get_llm_config('memory_retain') falls back to default_model.""" + monkeypatch.chdir(tmp_path) + + # Test fallback to default_model when retain model is not set + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o-mini +memory: + cerebral: + retain: + model: null +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config("memory_retain") + assert llm_config.model == "openai/gpt-4o-mini" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_max_iters_for_memory_modules(self, monkeypatch, tmp_path): + """get_max_iters uses memory.hippocampus.* config for memory_* names.""" + monkeypatch.chdir(tmp_path) + + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_max_iters: 3 +memory: + hippocampus: + distiller: + max_iters: 5 + cartographer: + max_iters: 7 +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + assert settings.get_max_iters("memory_distiller") == 5 + assert settings.get_max_iters("memory_cartographer") == 7 + # Unknown names fall back to signature lookup + assert settings.get_max_iters("code_review") == 3 # default + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_max_llm_calls_for_memory_modules(self, monkeypatch, tmp_path): + """get_max_llm_calls uses memory.hippocampus.* config for memory_* names.""" + monkeypatch.chdir(tmp_path) + + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_max_llm_calls: 5 +memory: + hippocampus: + distiller: + max_llm_calls: 10 + cartographer: + max_llm_calls: 15 +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + assert settings.get_max_llm_calls("memory_distiller") == 10 + assert settings.get_max_llm_calls("memory_cartographer") == 15 + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_consolidation(self, monkeypatch, tmp_path): + """get_llm_config('memory_consolidation') reads memory.cerebral.consolidation with fallback.""" + from codespy.config_memory import MEMORY_CONSOLIDATION + + monkeypatch.chdir(tmp_path) + + # Test with explicit consolidation model + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + cerebral: + retain: + model: anthropic/claude-3-opus + consolidation: + model: openai/gpt-4o-mini +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config(MEMORY_CONSOLIDATION) + assert llm_config.model == "openai/gpt-4o-mini" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_consolidation_fallback(self, monkeypatch, tmp_path): + """get_llm_config('memory_consolidation') falls back to retain model → default.""" + from codespy.config_memory import MEMORY_CONSOLIDATION + + monkeypatch.chdir(tmp_path) + + # Test fallback to retain model + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + cerebral: + retain: + model: anthropic/claude-3-opus + consolidation: + model: null +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config(MEMORY_CONSOLIDATION) + assert llm_config.model == "anthropic/claude-3-opus" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_consolidation_fallback_to_default(self, monkeypatch, tmp_path): + """get_llm_config('memory_consolidation') falls back to default when retain is also null.""" + from codespy.config_memory import MEMORY_CONSOLIDATION + + monkeypatch.chdir(tmp_path) + + # Test fallback to default when both consolidation and retain are null + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o-mini +memory: + cerebral: + retain: + model: null + consolidation: + model: null +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config(MEMORY_CONSOLIDATION) + assert llm_config.model == "openai/gpt-4o-mini" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_mental_models(self, monkeypatch, tmp_path): + """get_llm_config('memory_mental_models') reads memory.cerebral.mental_models with fallback.""" + from codespy.config_memory import MEMORY_MENTAL_MODELS + + monkeypatch.chdir(tmp_path) + + # Test with explicit mental_models model + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + cerebral: + retain: + model: anthropic/claude-3-opus + mental_models: + model: anthropic/claude-3-haiku + prefrontal: + recall: + model: openai/gpt-4o-mini +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config(MEMORY_MENTAL_MODELS) + assert llm_config.model == "anthropic/claude-3-haiku" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) + + def test_get_llm_config_for_memory_mental_models_fallback(self, monkeypatch, tmp_path): + """get_llm_config('memory_mental_models') falls back to prefrontal.recall → retain → default.""" + from codespy.config_memory import MEMORY_MENTAL_MODELS + + monkeypatch.chdir(tmp_path) + + # Test fallback chain + config_file = tmp_path / "codespy.yaml" + config_file.write_text(""" +llm: + default_model: openai/gpt-4o +memory: + cerebral: + retain: + model: anthropic/claude-3-opus + mental_models: + model: null + prefrontal: + recall: + model: openai/gpt-4o-mini +""") + monkeypatch.setenv("CODESPY_CONFIG", str(config_file)) + + settings = Settings() + llm_config = settings.get_llm_config(MEMORY_MENTAL_MODELS) + assert llm_config.model == "openai/gpt-4o-mini" + + monkeypatch.delenv("CODESPY_CONFIG", raising=False) diff --git a/tests/test_config_memory.py b/tests/test_config_memory.py index dbbf27c..75674c2 100644 --- a/tests/test_config_memory.py +++ b/tests/test_config_memory.py @@ -1,122 +1,90 @@ """Tests for memory storage configuration and access verification.""" +import os from unittest.mock import MagicMock, patch import pytest +from pydantic import ValidationError from codespy.config_memory import ( + EMBEDDING_MODELS, + CerebralConsolidationConfig, + CerebralRetainConfig, + MentalModelsConfig, PostgresConfig, - Pg0Config, - _generate_bank_id, - apply_memory_env_overrides, + _apply_hindsight_llm_call_defaults, + _cerebral_litellm_params, get_episode_store, - reset_episode_store, - verify_memory_access, ) class TestGenerateBankId: - """Tests for _generate_bank_id function.""" + """Tests for default bank_id behavior.""" - def test_generate_bank_id_returns_codespy(self): - """Should return 'codespy' as the default bank_id.""" - result = _generate_bank_id() - assert result == "codespy" + def test_default_bank_id_is_codebase(self): + """Bank ID defaults to 'codebase' when not set.""" + from codespy.config_memory import DEFAULT_BANK_ID + + settings = MagicMock() + settings.memory.bank_id = None + assert DEFAULT_BANK_ID == "codebase" + assert (settings.memory.bank_id or DEFAULT_BANK_ID) == "codebase" class TestApplyMemoryEnvOverrides: - """Tests for apply_memory_env_overrides function.""" + """Tests for memory env var overrides with new collapsed paths.""" 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"]["host"] == "myhost.example.com" + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - 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" + result = apply_env_overrides(config, _ENV_MAP) + assert result["memory"]["postgres"]["host"] == "myhost.example.com" - 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_memory_distiller_model(self, monkeypatch): + """MEMORY_DISTILLER_MODEL should set the distiller model (collapsed path).""" + monkeypatch.setenv("MEMORY_DISTILLER_MODEL", "claude-3-sonnet") + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - 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" + result = apply_env_overrides(config, _ENV_MAP) + assert result["memory"]["hippocampus"]["distiller"]["model"] == "claude-3-sonnet" - 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_memory_embeddings_model(self, monkeypatch): + """MEMORY_EMBEDDINGS_MODEL should set embeddings model (collapsed path).""" + monkeypatch.setenv("MEMORY_EMBEDDINGS_MODEL", "openai/text-embedding-3-large") + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - 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 + result = apply_env_overrides(config, _ENV_MAP) + assert result["memory"]["cerebral"]["embeddings"]["model"] == "openai/text-embedding-3-large" - 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_memory_max_hippocampus_tokens(self, monkeypatch): + """MEMORY_MAX_HIPPOCAMPUS_TOKENS should set hippocampus token budget (collapsed path).""" + monkeypatch.setenv("MEMORY_MAX_HIPPOCAMPUS_TOKENS", "1234") + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - 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 + result = apply_env_overrides(config, _ENV_MAP) + # convert_env_value returns numbers as raw strings + assert result["memory"]["hippocampus"]["max_hippocampus_tokens"] == "1234" - 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_override_memory_max_hippocampus_item_tokens(self, monkeypatch): + """MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS should set hippocampus item token budget (collapsed path).""" + monkeypatch.setenv("MEMORY_MAX_HIPPOCAMPUS_ITEM_TOKENS", "256") + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - def test_no_memory_prefix_ignored(self, monkeypatch): - """Non-MEMORY_ env vars should be ignored.""" - monkeypatch.setenv("OTHER_POSTGRES_HOST", "myhost.example.com") config = {} - result = apply_memory_env_overrides(config) - assert "memory" not in result or "postgres" not in result.get("memory", {}) + result = apply_env_overrides(config, _ENV_MAP) + # convert_env_value returns numbers as raw strings + assert result["memory"]["hippocampus"]["max_hippocampus_item_tokens"] == "256" class TestPostgresConfigBuildUri: @@ -147,54 +115,6 @@ def test_build_uri_no_password(self): 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_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 "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.""" - 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.""" @@ -204,7 +124,6 @@ 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 @@ -222,170 +141,685 @@ 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 postgres.build_uri() returns a URI.""" - 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() - - 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://u:p@host:5432/codespy", "test-bank", schema="episodic" - ) - assert result == mock_store +class TestCerebralLitellmParams: + """Tests for _cerebral_litellm_params function.""" - def test_caching_behavior(self): - """Should cache the store after first call.""" + @pytest.fixture + def mock_settings(self): + """Create a mock settings object with LLM config.""" settings = MagicMock() - settings.memory.postgres = MagicMock() - settings.memory.postgres.build_uri.return_value = "postgresql://localhost:5432/codespy" - settings.memory.bank_id = "test-bank" + settings.llm.openai_api_key.get_secret_value.return_value = "sk-openai-test" + settings.llm.openai_api_base = "https://api.openai.com/v1" + settings.llm.anthropic_api_key.get_secret_value.return_value = "sk-anthropic-test" + settings.llm.gemini_api_key.get_secret_value.return_value = "gemini-test" + settings.llm.azure_api_key.get_secret_value.return_value = "azure-test" + settings.llm.azure_api_base = "https://my-resource.openai.azure.com" + return settings - mock_store = MagicMock() + def test_openai_prefix(self, mock_settings): + """openai/ prefix should use OpenAI credentials.""" + mock_settings.get_llm_config.return_value.model = "openai/gpt-4" - with patch("codespy.config_memory._store", mock_store): - with patch("codespy.config_memory._store_built", True): - result = get_episode_store(settings) + model, api_key, base_url = _cerebral_litellm_params(mock_settings) - # Should return cached store without creating new one - assert result == mock_store + assert model == "openai/gpt-4" + assert api_key == "sk-openai-test" + assert base_url == "https://api.openai.com/v1" + def test_anthropic_prefix(self, mock_settings): + """anthropic/ prefix should use Anthropic credentials.""" + mock_settings.get_llm_config.return_value.model = "anthropic/claude-3-opus-4-1" -class TestResetEpisodeStore: - """Tests for reset_episode_store function.""" + model, api_key, base_url = _cerebral_litellm_params(mock_settings) - def test_closes_existing_store(self): - """Should close existing store and reset cache.""" - mock_store = MagicMock() + assert model == "anthropic/claude-3-opus-4-1" + assert api_key == "sk-anthropic-test" + assert base_url is None - 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() +class TestEmbeddingModels: + """Tests for EMBEDDING_MODELS constant.""" - def test_handles_close_exception(self): - """Should handle exceptions during close gracefully.""" - mock_store = MagicMock() - mock_store.close.side_effect = Exception("Close failed") + def test_embedding_models_mapping_bedrock(self): + """EMBEDDING_MODELS should have entry for bedrock.""" + assert "bedrock" in EMBEDDING_MODELS + assert "embed-v4:0" in EMBEDDING_MODELS["bedrock"] - with patch("codespy.config_memory._store", mock_store): - with patch("codespy.config_memory._store_built", True): - # Should not raise - reset_episode_store() + def test_embedding_models_mapping_openai(self): + """EMBEDDING_MODELS should have entry for openai.""" + assert "openai" in EMBEDDING_MODELS + assert "text-embedding-3-small" in EMBEDDING_MODELS["openai"] - mock_store.close.assert_called_once() + def test_embedding_models_mapping_anthropic(self): + """EMBEDDING_MODELS should have entry for anthropic.""" + assert "anthropic" in EMBEDDING_MODELS + def test_embedding_models_mapping_gemini(self): + """EMBEDDING_MODELS should have entry for gemini.""" + assert "gemini" in EMBEDDING_MODELS -class TestVerifyMemoryAccess: - """Tests for verify_memory_access function.""" + def test_embedding_models_mapping_azure(self): + """EMBEDDING_MODELS should have entry for azure.""" + assert "azure" in EMBEDDING_MODELS - def test_verify_memory_access_all_disabled(self): - """When all signatures disabled, returns success with skip message.""" - settings = MagicMock() - settings.is_signature_enabled.return_value = False - settings.get_memory_enabled.return_value = False - success, message = verify_memory_access(settings) +class TestCerebralRetainConfig: + """Tests for CerebralRetainConfig chunk_size validation.""" - assert success is True - assert "Memory disabled" in message + def test_default_chunk_size(self): + """Default chunk_size should be 12288.""" + config = CerebralRetainConfig() + assert config.chunk_size == 12288 - def test_verify_memory_access_enabled_sig_disabled(self): - """When signature has memory enabled but signature itself is disabled.""" - settings = MagicMock() + def test_chunk_size_accepted_values(self): + """Valid chunk sizes should be accepted.""" + config = CerebralRetainConfig(chunk_size=5000) + assert config.chunk_size == 5000 - def is_enabled(sig): - return False # All signatures disabled + config = CerebralRetainConfig(chunk_size=63999) + assert config.chunk_size == 63999 - def memory_enabled(sig): - return True # But memory is configured + config = CerebralRetainConfig(chunk_size=1) + assert config.chunk_size == 1 - settings.is_signature_enabled.side_effect = is_enabled - settings.get_memory_enabled.side_effect = memory_enabled + def test_chunk_size_rejects_zero(self): + """chunk_size=0 should raise ValidationError.""" + with pytest.raises(ValidationError): + CerebralRetainConfig(chunk_size=0) - success, message = verify_memory_access(settings) + def test_chunk_size_rejects_negative(self): + """Negative chunk_size should raise ValidationError.""" + with pytest.raises(ValidationError): + CerebralRetainConfig(chunk_size=-1) - assert success is True - assert "Memory disabled" in message + def test_chunk_size_rejects_64000(self): + """chunk_size=64000 should raise ValidationError (must be < 64000).""" + with pytest.raises(ValidationError): + CerebralRetainConfig(chunk_size=64000) - def test_verify_memory_access_store_none(self): - """When memory active but store is None (no PostgreSQL configured).""" - settings = MagicMock() + def test_chunk_size_rejects_too_large(self): + """chunk_size > 64000 should raise ValidationError.""" + with pytest.raises(ValidationError): + CerebralRetainConfig(chunk_size=100000) - def is_enabled(sig): - return sig == "summary" # Only summary enabled - def memory_enabled(sig): - return sig == "summary" # Memory enabled for summary +class TestCerebralRetainChunkSizeEnvOverride: + """Tests for MEMORY_RETAIN_CHUNK_SIZE env var override.""" - settings.is_signature_enabled.side_effect = is_enabled - settings.get_memory_enabled.side_effect = memory_enabled + def test_override_memory_retain_chunk_size(self, monkeypatch): + """MEMORY_RETAIN_CHUNK_SIZE should set memory.cerebral.retain.chunk_size.""" + monkeypatch.setenv("MEMORY_RETAIN_CHUNK_SIZE", "5000") + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides - with patch("codespy.config_memory.get_episode_store", return_value=None): - success, message = verify_memory_access(settings) + config = {} + result = apply_env_overrides(config, _ENV_MAP) + assert result["memory"]["cerebral"]["retain"]["chunk_size"] == "5000" + + +class TestPrefrontalConfig: + """Tests for memory.prefrontal config, env mapping and get_cerebral wiring.""" + + ENV = { + "MEMORY_PREFRONTAL_REFLECTS": ("reflects", "5"), + "MEMORY_RECALL_MODEL": ("model", "openai/gpt-4o-mini"), + "MEMORY_RECALL_REACH": ("reach", "bank"), + "MEMORY_RECALL_MAX_TOKENS": ("max_tokens", "2000"), + "MEMORY_RECALL_MAX_TOOL_TOKENS": ("max_tool_tokens", "700"), + "MEMORY_RECALL_MAX_TOOL_CALLS": ("max_tool_calls", "2"), + } + + def test_defaults(self): + from codespy.config_memory import MemoryConfig + + pf = MemoryConfig().prefrontal + assert pf.reflects == 3 # Changed from 5 to 3 + assert pf.recall.model == "bedrock/converse/nvidia.nemotron-super-3-120b" + assert pf.recall.reach == "org" + assert pf.recall.max_tokens == 8192 + assert pf.recall.max_tool_tokens == 2048 + assert pf.recall.max_tool_calls == 0 + + def test_env_mapping(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import PrefrontalConfig + from codespy.config_utils import apply_env_overrides + + for env, (_, value) in self.ENV.items(): + monkeypatch.setenv(env, value) + result = apply_env_overrides({}, _ENV_MAP)["memory"]["prefrontal"] + + # Verify env mapping for reflects + assert _ENV_MAP["MEMORY_PREFRONTAL_REFLECTS"] == ("memory", "prefrontal", "reflects") + assert "reflects" in result + + # Verify env mapping for recall settings + assert _ENV_MAP["MEMORY_RECALL_MODEL"] == ("memory", "prefrontal", "recall", "model") + assert _ENV_MAP["MEMORY_RECALL_REACH"] == ("memory", "prefrontal", "recall", "reach") + assert _ENV_MAP["MEMORY_RECALL_MAX_TOKENS"] == ("memory", "prefrontal", "recall", "max_tokens") + assert "recall" in result + assert result["recall"]["model"] == "openai/gpt-4o-mini" + assert result["recall"]["reach"] == "bank" + + cfg = PrefrontalConfig(**result) + assert cfg.recall.reach == "bank" + assert cfg.reflects == 5 + assert cfg.recall.max_tool_calls == 2 + + def test_rejects_negative_reflects(self): + from codespy.config_memory import PrefrontalConfig + + with pytest.raises(ValidationError): + PrefrontalConfig(reflects=-1) + + def test_reflection_modules_unchanged(self): + from codespy.config_memory import REFLECTION_MODULES + + assert REFLECTION_MODULES == ("memory_distiller", "memory_cartographer") + + def test_apply_reflect_config_returns_empty_when_reflects_zero(self): + from codespy.config_memory import PrefrontalConfig, _apply_reflect_config + + cfg = PrefrontalConfig(reflects=0) + result = _apply_reflect_config(cfg) + assert result == {} + + def test_apply_reflect_config_applies_values(self, monkeypatch): + pytest = __import__("pytest") + ha = pytest.importorskip("hindsight_api") + from codespy.config_memory import PrefrontalConfig, _apply_reflect_config + + cfg = PrefrontalConfig(reflects=3) + # Clear env vars to ensure config values are applied + monkeypatch.delenv("HINDSIGHT_API_REFLECT_MAX_ITERATIONS", raising=False) + + result = _apply_reflect_config(cfg) + + # With reflects=3, the cap is set to 2*3=6 (doubled for LOW budget) + assert result["iterations"] == 6 + assert result["budget"] == "low" + + def test_apply_reflect_config_env_wins_over_config(self, monkeypatch): + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") + from codespy.config_memory import ( + HINDSIGHT_REFLECT_MAX_ITERATIONS_ENV, + HINDSIGHT_REFLECT_MAX_CONTEXT_TOKENS_ENV, + PrefrontalConfig, + _apply_reflect_config, + ) - assert success is False - assert "not configured" in message + monkeypatch.setenv(HINDSIGHT_REFLECT_MAX_ITERATIONS_ENV, "10") + monkeypatch.setenv(HINDSIGHT_REFLECT_MAX_CONTEXT_TOKENS_ENV, "20000") + + cfg = PrefrontalConfig(reflects=3) + result = _apply_reflect_config(cfg) + + assert result["iterations"] == 10 # env wins (used as-is, not doubled) + assert result["budget"] == "low" + + def test_apply_hindsight_llm_call_defaults(self, monkeypatch): + """Test _apply_hindsight_llm_call_defaults sets global/reflect timeout/retries.""" + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") + import hindsight_api.config as ha_cfg + + # Clear all relevant env vars + for env in [ + "HINDSIGHT_API_LLM_TIMEOUT", + "HINDSIGHT_API_LLM_MAX_RETRIES", + "HINDSIGHT_API_REFLECT_LLM_TIMEOUT", + "HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES", + ]: + monkeypatch.delenv(env, raising=False) + + raw = ha_cfg._get_raw_config() + + # Save original values to restore after test + orig_llm_timeout = raw.llm_timeout + orig_llm_max_retries = raw.llm_max_retries + orig_reflect_llm_timeout = raw.reflect_llm_timeout + orig_reflect_llm_max_retries = raw.reflect_llm_max_retries + + try: + raw.llm_timeout = 120.0 + raw.llm_max_retries = 3 + raw.reflect_llm_timeout = 30.0 + raw.reflect_llm_max_retries = 3 + + settings = MagicMock() + settings.llm.timeout = 240.0 + settings.llm.retries = 2 + + _apply_hindsight_llm_call_defaults(settings) + + # Verify global and reflect settings were updated + assert raw.llm_timeout == 240.0 + assert raw.llm_max_retries == 2 + assert raw.reflect_llm_timeout == 240.0 + assert raw.reflect_llm_max_retries == 2 + finally: + # Restore original values + raw.llm_timeout = orig_llm_timeout + raw.llm_max_retries = orig_llm_max_retries + raw.reflect_llm_timeout = orig_reflect_llm_timeout + raw.reflect_llm_max_retries = orig_reflect_llm_max_retries + + def test_apply_hindsight_llm_call_defaults_with_env_vars(self, monkeypatch): + """Test that when env vars are set, function uses those values and does not overwrite. + + Note: Hindsight loads env vars at import time and caches the config. + This test verifies that when env vars are present, the function + correctly identifies them and does not overwrite the values. + """ + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") + import hindsight_api.config as ha_cfg + + raw = ha_cfg._get_raw_config() + + # Save original values + orig_llm_timeout = raw.llm_timeout + orig_llm_max_retries = raw.llm_max_retries + orig_reflect_llm_timeout = raw.reflect_llm_timeout + orig_reflect_llm_max_retries = raw.reflect_llm_max_retries + + try: + # Set up values that would be overwritten if env is not checked + raw.llm_timeout = 120.0 + raw.llm_max_retries = 3 + raw.reflect_llm_timeout = 30.0 + raw.reflect_llm_max_retries = 3 + + settings = MagicMock() + settings.llm.timeout = 240.0 + settings.llm.retries = 3 + + _apply_hindsight_llm_call_defaults(settings) + + # Verify the function logic based on env state + if os.environ.get("HINDSIGHT_API_REFLECT_LLM_TIMEOUT"): + # If env is set, function should not overwrite + assert raw.reflect_llm_timeout == 30.0 # unchanged + else: + # If env is not set, function should set from settings + assert raw.reflect_llm_timeout == 240.0 + + if os.environ.get("HINDSIGHT_API_LLM_TIMEOUT"): + # If global env is set, function should not overwrite + assert raw.llm_timeout == 120.0 # unchanged + else: + # If env is not set, function should set from settings + assert raw.llm_timeout == 240.0 + finally: + # Restore original values + raw.llm_timeout = orig_llm_timeout + raw.llm_max_retries = orig_llm_max_retries + raw.reflect_llm_timeout = orig_reflect_llm_timeout + raw.reflect_llm_max_retries = orig_reflect_llm_max_retries + + def test_consolidation_inherits_global_llm_timeout(self, monkeypatch): + """Test that consolidation inherits global llm_timeout when per-op value is None.""" + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") + import hindsight_api.config as ha_cfg + + # Clear all env vars + for env in [ + "HINDSIGHT_API_LLM_TIMEOUT", + "HINDSIGHT_API_LLM_MAX_RETRIES", + "HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT", + "HINDSIGHT_API_REFLECT_LLM_TIMEOUT", + ]: + monkeypatch.delenv(env, raising=False) + + raw = ha_cfg._get_raw_config() + + # Save original values + orig_llm_timeout = raw.llm_timeout + orig_consolidation_llm_timeout = raw.consolidation_llm_timeout + + try: + # Simulate Hindsight's behavior: per-op is None, falls back to global + # Set consolidation_llm_timeout to None (as Hindsight would when env is unset) + raw.consolidation_llm_timeout = None + raw.llm_timeout = 240.0 + + # Hindsight's _op_defaults returns: per-op if non-None, else global + # This is what MemoryEngine does for consolidation + per_op = raw.consolidation_llm_timeout + effective = per_op if per_op is not None else raw.llm_timeout + assert effective == 240.0 + finally: + # Restore original values + raw.llm_timeout = orig_llm_timeout + raw.consolidation_llm_timeout = orig_consolidation_llm_timeout + + def _settings(self, reflects): + from codespy.config_memory import MemoryConfig - def test_verify_memory_access_postgres_ok(self): - """When memory active with valid PostgreSQL store.""" settings = MagicMock() - settings.memory.bank_id = "test-bank" - - 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 EpisodeStore that verifies successfully - mock_store = MagicMock() - mock_store.verify_access.return_value = None + settings.memory = MemoryConfig() + settings.memory.prefrontal.reflects = reflects + settings.memory.postgres.host = "db" + settings.get_llm_config.return_value = MagicMock(model="openai/gpt-4o") + settings.llm.openai_api_key = None + settings.llm.openai_api_base = None + return settings + + # Note: Removed old test_get_cerebral_sets_env_before_import - now using programmatic + # config via _apply_reflect_config instead of env vars. See tests for _apply_reflect_config. + + def _capture_cerebral_kwargs(self, monkeypatch, settings) -> dict: + import codespy.config_memory as cm + from codespy.agents.memory import cerebral as cerebral_module + + seen: dict = {} + + def fake_cerebral(**kwargs): + seen.update(kwargs) + return MagicMock() + + monkeypatch.setattr(cm, "_cerebral", None) + monkeypatch.setattr(cm, "_cerebral_built", False) + monkeypatch.setattr(cerebral_module, "Cerebral", fake_cerebral) + assert cm.get_cerebral(settings) is not None + monkeypatch.setattr(cm, "_cerebral", None) + monkeypatch.setattr(cm, "_cerebral_built", False) + return seen + + def test_get_cerebral_without_prefrontal_model_passes_no_reflect_llm(self, monkeypatch): + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") # Skip if hindsight_api not available + kwargs = self._capture_cerebral_kwargs(monkeypatch, self._settings(0)) + assert not any(k.startswith("reflect_llm_") for k in kwargs) + + def test_get_cerebral_passes_prefrontal_recall_model_as_reflect_llm(self, monkeypatch): + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") # Skip if hindsight_api not available + from pydantic import SecretStr + + settings = self._settings(3) + settings.memory.prefrontal.recall.model = "anthropic/claude-sonnet-4-5" + settings.llm.anthropic_api_key = SecretStr("sk-ant") + kwargs = self._capture_cerebral_kwargs(monkeypatch, settings) + # Engine model is unchanged; reflect gets its own model and credentials. + assert kwargs["llm_model"] == "openai/gpt-4o" + # Cerebral derives reflect_llm_provider from llm_provider itself; not passed by caller + assert "reflect_llm_provider" not in kwargs + assert kwargs["reflect_llm_model"] == "anthropic/claude-sonnet-4-5" + assert kwargs["reflect_llm_api_key"] == "sk-ant" + assert kwargs["reflect_llm_base_url"] is None + + def test_get_cerebral_reflect_kwargs_match_cerebral_signature(self, monkeypatch): + """Guard against signature drift: reflect kwargs must bind to Cerebral.__init__. + + Skipped when hindsight_api is not installed (e.g., in CI without the dependency). + """ + import inspect + + from pydantic import SecretStr + + # Skip if hindsight_api is not available (Cerebral requires it) + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") + from codespy.agents.memory.cerebral.cerebral import Cerebral + + settings = self._settings(3) + + # Test without prefrontal model + settings_no_pf = self._settings(3) + settings_no_pf.memory.prefrontal.model = None + kwargs_no_pf = self._capture_cerebral_kwargs(monkeypatch, settings_no_pf) + # Should bind without error + inspect.signature(Cerebral.__init__).bind(None, **kwargs_no_pf) + + # Test with prefrontal recall model + settings.memory.prefrontal.recall.model = "anthropic/claude-sonnet-4-5" + settings.llm.anthropic_api_key = SecretStr("sk-ant") + kwargs_with_pf = self._capture_cerebral_kwargs(monkeypatch, settings) + # Should bind without error + inspect.signature(Cerebral.__init__).bind(None, **kwargs_with_pf) + + def test_get_cerebral_passes_max_observations_per_scope(self, monkeypatch): + """Test that get_cerebral passes max_observations_per_scope to Cerebral.""" + pytest = __import__("pytest") + pytest.importorskip("hindsight_api") # Skip if hindsight_api not available + + settings = self._settings(0) + settings.memory.cerebral.consolidation.max_observations_per_scope = 42 + kwargs = self._capture_cerebral_kwargs(monkeypatch, settings) + assert kwargs["max_observations_per_scope"] == 42 + + +class TestPrefrontalModelResolution: + """get_llm_config('memory_recall') fallback chain. + + Settings are built with explicit sections so a local ``.env`` cannot + override them. + """ + + def _settings(self, *, recall_model=None, retain=None): + from codespy.config import Settings + from codespy.config_llm import LLMConfig + from codespy.config_memory import ( + CerebralConfig, + CerebralRetainConfig, + MemoryConfig, + PrefrontalConfig, + RecallConfig, + ) - with patch("codespy.config_memory.get_episode_store", return_value=mock_store): - success, message = verify_memory_access(settings) + return Settings( + llm=LLMConfig(default_model="openai/gpt-4o"), + memory=MemoryConfig( + cerebral=CerebralConfig(retain=CerebralRetainConfig(model=retain)), + prefrontal=PrefrontalConfig(recall=RecallConfig(model=recall_model)), + ), + ) - assert success is True - assert "verified" in message - assert "PostgreSQL" in message - mock_store.verify_access.assert_called_once() + def test_explicit_recall_model(self): + from codespy.config_memory import MEMORY_RECALL - def test_verify_memory_access_raises(self): - """When store's verify_access raises an exception.""" - settings = MagicMock() - settings.memory.bank_id = "test-bank" + s = self._settings(recall_model="anthropic/claude-haiku", retain="openai/gpt-4o-mini") + assert s.get_llm_config(MEMORY_RECALL).model == "anthropic/claude-haiku" - def is_enabled(sig): - return sig == "summary" + def test_falls_back_to_retain_model(self): + from codespy.config_memory import MEMORY_RECALL - def memory_enabled(sig): - return sig == "summary" + s = self._settings(retain="openai/gpt-4o-mini") + assert s.get_llm_config(MEMORY_RECALL).model == "openai/gpt-4o-mini" + + def test_falls_back_to_default_model(self): + from codespy.config_memory import MEMORY_RECALL + + s = self._settings() + assert s.get_llm_config(MEMORY_RECALL).model == s.llm.default_model - settings.is_signature_enabled.side_effect = is_enabled - settings.get_memory_enabled.side_effect = memory_enabled + # Note: _warn_reflect_mismatch removed - now using programmatic _apply_reflect_config + # which sets values directly instead of warning about mismatches + + +class TestConsolidationConfig: + """Tests for memory.cerebral.consolidation config and env mapping.""" + + def test_defaults(self): + from codespy.config_memory import CerebralConfig + + cfg = CerebralConfig().consolidation + assert cfg.model == "bedrock/converse/nvidia.nemotron-super-3-120b" + assert cfg.max_observations_per_scope == 100 # default + + def test_env_mapping(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import CerebralConfig + from codespy.config_utils import apply_env_overrides - # Mock store that raises on verify_access - mock_store = MagicMock() - mock_store.verify_access.side_effect = ConnectionError("Cannot connect") + monkeypatch.setenv("MEMORY_CONSOLIDATION_MODEL", "openai/gpt-4o-mini") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["cerebral"]["consolidation"] + assert _ENV_MAP["MEMORY_CONSOLIDATION_MODEL"] == ("memory", "cerebral", "consolidation", "model") + assert result["model"] == "openai/gpt-4o-mini" + cfg = CerebralConsolidationConfig(**result) + assert cfg.model == "openai/gpt-4o-mini" - with patch("codespy.config_memory.get_episode_store", return_value=mock_store): - success, message = verify_memory_access(settings) + def test_max_observations_per_scope_default(self): + """Default max_observations_per_scope is 100.""" + cfg = CerebralConsolidationConfig() + assert cfg.max_observations_per_scope == 100 - assert success is False - assert "not accessible" in message - assert "Cannot connect" in message + def test_max_observations_per_scope_custom(self): + """Custom max_observations_per_scope is accepted.""" + cfg = CerebralConsolidationConfig(max_observations_per_scope=50) + assert cfg.max_observations_per_scope == 50 + + def test_max_observations_per_scope_unlimited(self): + """-1 means unlimited.""" + cfg = CerebralConsolidationConfig(max_observations_per_scope=-1) + assert cfg.max_observations_per_scope == -1 + + def test_max_observations_per_scope_rejects_less_than_minus_one(self): + """Values < -1 are rejected.""" + with pytest.raises(ValidationError): + CerebralConsolidationConfig(max_observations_per_scope=-2) + + def test_max_observations_per_scope_env_mapping(self, monkeypatch): + """MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE env var works.""" + from codespy.config import _ENV_MAP + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_CONSOLIDATION_MAX_OBSERVATIONS_PER_SCOPE", "200") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["cerebral"]["consolidation"] + assert result["max_observations_per_scope"] == "200" + cfg = CerebralConsolidationConfig(**result) + assert cfg.max_observations_per_scope == 200 + + +class TestRecallConfig: + """Tests for memory.prefrontal.recall config and env mapping.""" + + def test_defaults(self): + from codespy.config_memory import RecallConfig + + cfg = RecallConfig() + assert cfg.model == "bedrock/converse/nvidia.nemotron-super-3-120b" + assert cfg.reach == "org" + assert cfg.max_tokens == 8192 + assert cfg.max_tool_tokens == 2048 + assert cfg.max_tool_calls == 0 + + def test_env_mapping_model(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import RecallConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_RECALL_MODEL", "openai/gpt-4o") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["prefrontal"]["recall"] + assert _ENV_MAP["MEMORY_RECALL_MODEL"] == ("memory", "prefrontal", "recall", "model") + assert result["model"] == "openai/gpt-4o" + cfg = RecallConfig(**result) + assert cfg.model == "openai/gpt-4o" + + def test_env_mapping_reach(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import RecallConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_RECALL_REACH", "bank") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["prefrontal"]["recall"] + assert _ENV_MAP["MEMORY_RECALL_REACH"] == ("memory", "prefrontal", "recall", "reach") + assert result["reach"] == "bank" + cfg = RecallConfig(**result) + assert cfg.reach == "bank" + + def test_env_mapping_max_tokens(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import RecallConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_RECALL_MAX_TOKENS", "4096") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["prefrontal"]["recall"] + assert _ENV_MAP["MEMORY_RECALL_MAX_TOKENS"] == ("memory", "prefrontal", "recall", "max_tokens") + assert result["max_tokens"] == "4096" + cfg = RecallConfig(**result) + assert cfg.max_tokens == 4096 + + def test_rejects_invalid_reach(self): + from codespy.config_memory import RecallConfig + from pydantic import ValidationError + + with pytest.raises(ValidationError): + RecallConfig(reach="world") + + +class TestMentalModelsConfig: + """Tests for memory.cerebral.mental_models config and env mapping.""" + + def test_defaults(self): + from codespy.config_memory import CerebralConfig + + cfg = CerebralConfig().mental_models + assert cfg.model == "bedrock/converse/nvidia.nemotron-super-3-120b" + assert cfg.max_tokens == 2048 + assert cfg.min_refresh_seconds == 0 + + def test_env_mapping(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import CerebralConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_MENTAL_MODELS_MODEL", "anthropic/claude-3-haiku") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["cerebral"]["mental_models"] + assert _ENV_MAP["MEMORY_MENTAL_MODELS_MODEL"] == ("memory", "cerebral", "mental_models", "model") + assert result["model"] == "anthropic/claude-3-haiku" + cfg = MentalModelsConfig(**result) + assert cfg.model == "anthropic/claude-3-haiku" + + def test_env_mapping_max_tokens(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import CerebralConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_MENTAL_MODELS_MAX_TOKENS", "1024") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["cerebral"]["mental_models"] + assert _ENV_MAP["MEMORY_MENTAL_MODELS_MAX_TOKENS"] == ("memory", "cerebral", "mental_models", "max_tokens") + assert result["max_tokens"] == "1024" + cfg = MentalModelsConfig(**result) + assert cfg.max_tokens == 1024 + + def test_env_mapping_min_refresh_seconds(self, monkeypatch): + from codespy.config import _ENV_MAP + from codespy.config_memory import CerebralConfig + from codespy.config_utils import apply_env_overrides + + monkeypatch.setenv("MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS", "60") + result = apply_env_overrides({}, _ENV_MAP)["memory"]["cerebral"]["mental_models"] + assert _ENV_MAP["MEMORY_MENTAL_MODELS_MIN_REFRESH_SECONDS"] == ("memory", "cerebral", "mental_models", "min_refresh_seconds") + assert result["min_refresh_seconds"] == "60" + cfg = MentalModelsConfig(**result) + assert cfg.min_refresh_seconds == 60 + + def test_rejects_zero_max_tokens(self): + from codespy.config_memory import MentalModelsConfig + + with pytest.raises(ValidationError): + MentalModelsConfig(max_tokens=0) + + def test_rejects_negative_max_tokens(self): + from codespy.config_memory import MentalModelsConfig + + with pytest.raises(ValidationError): + MentalModelsConfig(max_tokens=-1) + + def test_rejects_negative_min_refresh_seconds(self): + from codespy.config_memory import MentalModelsConfig + + with pytest.raises(ValidationError): + MentalModelsConfig(min_refresh_seconds=-1) + + def test_old_env_vars_not_in_env_map(self): + """Old mental_models env vars should not be in _ENV_MAP.""" + from codespy.config import _ENV_MAP + + assert "MEMORY_MAX_MENTAL_MODEL_TOKENS" not in _ENV_MAP + assert "MEMORY_MIN_MENTAL_MODEL_REFRESH_SECONDS" not in _ENV_MAP diff --git a/tests/test_context_safe.py b/tests/test_context_safe.py index 35fbd3b..f512302 100644 --- a/tests/test_context_safe.py +++ b/tests/test_context_safe.py @@ -7,10 +7,8 @@ from pydantic import ValidationError from codespy.agents.context_safe import ContextSafe -from codespy.config_dspy import ( - RLMFallbackConfig, - apply_rlm_fallback_env_overrides, -) +from codespy.config_dspy import RLMFallbackConfig +from codespy.config_utils import apply_env_overrides, build_env_map class TestRLMFallbackConfig: @@ -40,24 +38,30 @@ def test_threshold_bounds(self): class TestApplyRLMFallbackEnvOverrides: - """Tests for apply_rlm_fallback_env_overrides function.""" + """Tests for RLM env var overrides using new unified mechanism.""" def test_react_threshold_override(self): - """Test RLM_FALLBACK_REACT_THRESHOLD env var.""" + """Test RLM_FALLBACK_REACT_THRESHOLD env var (new name, no LLM_ prefix).""" + from codespy.config import _ENV_MAP + config = {} with patch.dict(os.environ, {"RLM_FALLBACK_REACT_THRESHOLD": "0.25"}): - result = apply_rlm_fallback_env_overrides(config) - assert result["rlm_fallback"]["react_threshold"] == "0.25" + result = apply_env_overrides(config, _ENV_MAP) + assert result["llm"]["rlm_fallback"]["react_threshold"] == "0.25" def test_enabled_override(self): - """Test RLM_FALLBACK_ENABLED env var.""" + """Test RLM_FALLBACK_ENABLED env var (new name, no LLM_ prefix).""" + from codespy.config import _ENV_MAP + config = {} with patch.dict(os.environ, {"RLM_FALLBACK_ENABLED": "false"}): - result = apply_rlm_fallback_env_overrides(config) - assert result["rlm_fallback"]["enabled"] is False + result = apply_env_overrides(config, _ENV_MAP) + assert result["llm"]["rlm_fallback"]["enabled"] is False def test_all_thresholds(self): - """Test all threshold env vars.""" + """Test all threshold env vars (new names, no LLM_ prefix).""" + from codespy.config import _ENV_MAP + config = {} env_vars = { "RLM_FALLBACK_REACT_THRESHOLD": "0.35", @@ -65,25 +69,33 @@ def test_all_thresholds(self): "RLM_FALLBACK_PREDICT_THRESHOLD": "0.55", } with patch.dict(os.environ, env_vars): - result = apply_rlm_fallback_env_overrides(config) - assert result["rlm_fallback"]["react_threshold"] == "0.35" - assert result["rlm_fallback"]["chain_of_thought_threshold"] == "0.45" - assert result["rlm_fallback"]["predict_threshold"] == "0.55" + result = apply_env_overrides(config, _ENV_MAP) + # Values are strings until pydantic coerces them + assert result["llm"]["rlm_fallback"]["react_threshold"] == "0.35" + assert result["llm"]["rlm_fallback"]["chain_of_thought_threshold"] == "0.45" + assert result["llm"]["rlm_fallback"]["predict_threshold"] == "0.55" def test_unrelated_env_vars_ignored(self): """Test that unrelated env vars are ignored.""" + from codespy.config import _ENV_MAP + config = {} with patch.dict(os.environ, {"OTHER_VAR": "value"}): - result = apply_rlm_fallback_env_overrides(config) - assert "rlm_fallback" not in result + result = apply_env_overrides(config, _ENV_MAP) + # rlm_fallback may not exist in result if no env vars matched + if "llm" in result: + assert "rlm_fallback" not in result.get("llm", {}) def test_existing_config_preserved(self): """Test that existing config is preserved.""" - config = {"rlm_fallback": {"enabled": False, "react_threshold": 0.20}} + from codespy.config import _ENV_MAP + + config = {"llm": {"rlm_fallback": {"enabled": False, "react_threshold": 0.20}}} with patch.dict(os.environ, {"RLM_FALLBACK_REACT_THRESHOLD": "0.35"}): - result = apply_rlm_fallback_env_overrides(config) - assert result["rlm_fallback"]["enabled"] is False - assert result["rlm_fallback"]["react_threshold"] == "0.35" + result = apply_env_overrides(config, _ENV_MAP) + assert result["llm"]["rlm_fallback"]["enabled"] is False + # Value is a string until pydantic coerces it + assert result["llm"]["rlm_fallback"]["react_threshold"] == "0.35" class MockSignature: @@ -100,10 +112,10 @@ def test_returns_correct_threshold_per_type(self): from codespy.config import Settings settings = Settings() - settings.rlm_fallback.enabled = True - settings.rlm_fallback.react_threshold = 0.30 - settings.rlm_fallback.chain_of_thought_threshold = 0.40 - settings.rlm_fallback.predict_threshold = 0.50 + settings.llm.rlm_fallback.enabled = True + settings.llm.rlm_fallback.react_threshold = 0.30 + settings.llm.rlm_fallback.chain_of_thought_threshold = 0.40 + settings.llm.rlm_fallback.predict_threshold = 0.50 assert settings.get_rlm_threshold("react") == 0.30 assert settings.get_rlm_threshold("chain_of_thought") == 0.40 @@ -114,7 +126,7 @@ def test_returns_1_0_when_disabled(self): from codespy.config import Settings settings = Settings() - settings.rlm_fallback.enabled = False + settings.llm.rlm_fallback.enabled = False assert settings.get_rlm_threshold("react") == 1.0 assert settings.get_rlm_threshold("chain_of_thought") == 1.0 @@ -125,7 +137,7 @@ def test_returns_1_0_for_unknown_type(self): from codespy.config import Settings settings = Settings() - settings.rlm_fallback.enabled = True + settings.llm.rlm_fallback.enabled = True assert settings.get_rlm_threshold("unknown_type") == 1.0 diff --git a/tests/test_cost_breakdown.py b/tests/test_cost_breakdown.py new file mode 100644 index 0000000..ad63012 --- /dev/null +++ b/tests/test_cost_breakdown.py @@ -0,0 +1,324 @@ +"""Tests for cost breakdown functionality: Review/Memory split tables.""" + +import pytest + +from codespy.config_memory import MEMORY_UNIT_PREFIX +from codespy.workflows.review.models import ReviewResult, SignatureStatsResult + + +class TestSignatureStatsResultIsMemory: + """Tests for SignatureStatsResult.is_memory property.""" + + def test_is_memory_true_for_memory_prefixed_names(self): + """Names starting with MEMORY_UNIT_PREFIX are memory signatures.""" + assert SignatureStatsResult(name=f"{MEMORY_UNIT_PREFIX}retain").is_memory is True + assert SignatureStatsResult(name=f"{MEMORY_UNIT_PREFIX}cartographer").is_memory is True + assert SignatureStatsResult(name=f"{MEMORY_UNIT_PREFIX}prefrontal").is_memory is True + + def test_is_memory_false_for_review_names(self): + """Names not starting with MEMORY_UNIT_PREFIX are review signatures.""" + assert SignatureStatsResult(name="code_review").is_memory is False + assert SignatureStatsResult(name="scope").is_memory is False + assert SignatureStatsResult(name="summary").is_memory is False + assert SignatureStatsResult(name="doc").is_memory is False + assert SignatureStatsResult(name="supply_chain").is_memory is False + assert SignatureStatsResult(name="audit").is_memory is False + + +class TestReviewResultCostBreakdownMarkdownLines: + """Tests for ReviewResult.cost_breakdown_markdown_lines.""" + + @staticmethod + def _make_review_stats(name: str, cost: float, calls: int = 1) -> SignatureStatsResult: + """Create a SignatureStatsResult with minimal fields set.""" + return SignatureStatsResult( + name=name, + cost=cost, + tokens=100 * calls, + call_count=calls, + duration_seconds=1.5 * calls, + input_tokens=50 * calls, + output_tokens=50 * calls, + input_cost=cost * 0.6, + output_cost=cost * 0.4, + ) + + def test_empty_signature_stats_returns_empty_list(self): + """Empty signature_stats should produce no breakdown.""" + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=[], + ) + assert result.cost_breakdown_markdown_lines() == [] + + def test_mixed_stats_render_two_tables_review_first(self): + """Mixed stats render Review table first, then Memory.""" + stats = [ + self._make_review_stats("scope", 0.5), + self._make_review_stats("code_review", 1.0), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}retain", 0.3), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}prefrontal", 0.2), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + ) + lines = result.cost_breakdown_markdown_lines() + text = "\n".join(lines) + + # Both tables should be present + assert "### Review" in text + assert "### Memory" in text + + # Review should come before Memory + assert text.index("### Review") < text.index("### Memory") + + # Each signature should appear in its correct table + # Review signatures + assert "| scope |" in text + assert "| code_review |" in text + # Memory signatures + assert f"| {MEMORY_UNIT_PREFIX}retain |" in text + assert f"| {MEMORY_UNIT_PREFIX}prefrontal |" in text + + def test_rows_sorted_by_cost_descending(self): + """Rows within each table should be sorted by cost descending.""" + stats = [ + self._make_review_stats("scope", 0.1), + self._make_review_stats("code_review", 0.5), + self._make_review_stats("summary", 0.3), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + ) + lines = result.cost_breakdown_markdown_lines() + + # Find the review section lines + review_start = next(i for i, line in enumerate(lines) if "### Review" in line) + # Find the first subtotal after review section + subtotal_idx = next( + i for i in range(review_start, len(lines)) if "**Subtotal:**" in lines[i] + ) + + # Extract table body (between header and subtotal) + # Skip header lines (heading, blank, header, separator) + table_lines = lines[review_start + 4 : subtotal_idx] + # Filter out empty lines + table_lines = [l for l in table_lines if l and l.startswith("|")] + + # Check order: code_review (0.5) should come before summary (0.3) before scope (0.1) + names = [line.split("|")[1].strip() for line in table_lines] + assert names == ["code_review", "summary", "scope"] + + def test_subtotals_calculated_correctly(self): + """Subtotals should sum costs and calls for each group.""" + stats = [ + self._make_review_stats("scope", 0.5, calls=2), + self._make_review_stats("code_review", 1.0, calls=3), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}retain", 0.3, calls=1), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}prefrontal", 0.2, calls=2), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + ) + lines = result.cost_breakdown_markdown_lines() + + # Find subtotal lines + subtotal_lines = [l for l in lines if "**Subtotal:**" in l] + assert len(subtotal_lines) == 2 + + # Review subtotal: 1.5 cost, 5 calls + review_subtotal = subtotal_lines[0] + assert "1.5000" in review_subtotal + assert "5" in review_subtotal # LLM Calls: 5 + + # Memory subtotal: 0.5 cost, 3 calls + memory_subtotal = subtotal_lines[1] + assert "0.5000" in memory_subtotal + assert "3" in memory_subtotal # LLM Calls: 3 + + def test_no_cache_references_in_output(self): + """Neither to_markdown nor cost_breakdown_markdown_lines should contain 'Cache'.""" + stats = [ + self._make_review_stats("scope", 0.5), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}retain", 0.3), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + ) + + # Check cost_breakdown_markdown_lines + lines = result.cost_breakdown_markdown_lines() + text = "\n".join(lines) + assert "Cache" not in text + assert "cache_read" not in text.lower() + assert "cache_write" not in text.lower() + + # Check full to_markdown output + full_md = result.to_markdown() + assert "Cache" not in full_md + assert "cache_read" not in full_md.lower() + assert "cache_write" not in full_md.lower() + + +class TestGitReporterCostBreakdown: + """Tests for GitReporter cost breakdown integration. + + These tests use cost_breakdown_markdown_lines() which is what + GitReporter._build_review_body uses internally for the cost tables. + """ + + @staticmethod + def _make_review_stats(name: str, cost: float, calls: int = 1) -> SignatureStatsResult: + return SignatureStatsResult( + name=name, + cost=cost, + tokens=100 * calls, + call_count=calls, + duration_seconds=1.5 * calls, + input_tokens=50 * calls, + output_tokens=50 * calls, + input_cost=cost * 0.6, + output_cost=cost * 0.4, + ) + + def test_build_review_body_no_cache_references(self): + """Cost breakdown should not contain 'Cache' references.""" + stats = [ + self._make_review_stats("scope", 0.5), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}retain", 0.3), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + total_cost=0.8, + llm_calls=2, + ) + + lines = result.cost_breakdown_markdown_lines("####") + body = "\n".join(lines) + + assert "Cache" not in body + assert "cache_read" not in body.lower() + assert "cache_write" not in body.lower() + # Should use #### heading level for cost tables + assert "#### Review" in body or "### Review" in body + + def test_only_review_stats_no_memory_heading(self): + """When only review stats exist, no Memory heading should appear.""" + stats = [ + self._make_review_stats("scope", 0.5), + self._make_review_stats("code_review", 1.0), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + total_cost=1.5, + llm_calls=2, + ) + lines = result.cost_breakdown_markdown_lines() + text = "\n".join(lines) + + assert "### Review" in text or "#### Review" in text # Review heading should be present + assert "### Memory" not in text and "#### Memory" not in text # Memory heading should NOT appear + + def test_only_memory_stats_no_review_heading(self): + """When only memory stats exist, no Review heading should appear.""" + stats = [ + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}retain", 0.3), + self._make_review_stats(f"{MEMORY_UNIT_PREFIX}prefrontal", 0.2), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + total_cost=0.5, + llm_calls=2, + ) + lines = result.cost_breakdown_markdown_lines() + text = "\n".join(lines) + + assert "### Memory" in text or "#### Memory" in text + assert "### Review" not in text and "#### Review" not in text + + +class TestReviewResultNoCacheFields: + """Tests that cache fields are not present in models.""" + + def test_signature_stats_result_no_cache_fields(self): + """SignatureStatsResult should not have cache fields.""" + stats = SignatureStatsResult(name="test", cost=0.5) + d = stats.model_dump() + assert "cache_read_tokens" not in d + assert "cache_write_tokens" not in d + + def test_cost_table_columns(self): + """Cost table should have correct columns (no Cache columns).""" + stats = [ + SignatureStatsResult( + name="test", + cost=0.5, + tokens=100, + call_count=2, + input_tokens=50, + output_tokens=50, + input_cost=0.3, + output_cost=0.2, + ), + ] + result = ReviewResult( + pr_number=1, + pr_title="Test", + pr_url="http://example.com/1", + repo="test/repo", + model_used="gpt-4", + signature_stats=stats, + ) + lines = result.cost_breakdown_markdown_lines() + # Find the header line + header_line = next(l for l in lines if "| Signature |" in l) + # Should have In Tokens, Out Tokens, In Cost, Out Cost, Calls, Duration + assert "Signature" in header_line + assert "In Tokens" in header_line + assert "Out Tokens" in header_line + assert "In Cost" in header_line + assert "Out Cost" in header_line + assert "Calls" in header_line + assert "Duration" in header_line + # Should NOT have cache columns + assert "Cache Read" not in header_line + assert "Cache Write" not in header_line diff --git a/tests/test_cost_tracker.py b/tests/test_cost_tracker.py new file mode 100644 index 0000000..ff6dc16 --- /dev/null +++ b/tests/test_cost_tracker.py @@ -0,0 +1,490 @@ +"""Tests for the CostTracker module.""" + +import threading +from unittest.mock import MagicMock, patch + +import pytest + +from codespy.agents.cost_tracker import ( + CostTracker, + SignatureContext, + SignatureStats, + _as_number, + _calculate_costs_from_entries, + _get_history_entries, + _get_history_uuids, + get_cost_tracker, +) + + +class TestSignatureStats: + """Tests for SignatureStats dataclass.""" + + def test_duration_seconds_returns_zero_if_no_start_time(self): + """Duration should be 0 if start_time is None.""" + stats = SignatureStats(name="test") + assert stats.duration_seconds == 0.0 + + def test_duration_seconds_calculates_from_start_time(self): + """Duration should calculate from start_time to now.""" + import time + + stats = SignatureStats(name="test", start_time=time.time() - 1.0) + assert stats.duration_seconds >= 0.9 + + def test_duration_seconds_uses_end_time_if_set(self): + """Duration should use end_time if set.""" + stats = SignatureStats(name="test", start_time=0.0, end_time=5.0) + assert stats.duration_seconds == 5.0 + + def test_duration_seconds_includes_external_duration(self): + """Duration should include external_duration_seconds.""" + stats = SignatureStats(name="test", start_time=0.0, end_time=5.0, external_duration_seconds=2.0) + assert stats.duration_seconds == 7.0 # 5.0 wall + 2.0 external + + def test_duration_seconds_external_only_when_no_start_time(self): + """Duration should be external_duration_seconds when start_time is None.""" + stats = SignatureStats(name="test", external_duration_seconds=3.0) + assert stats.duration_seconds == 3.0 + + def test_to_dict_includes_all_fields(self): + """to_dict should include all relevant fields.""" + stats = SignatureStats( + name="test", cost=0.5, tokens=1000, call_count=5, start_time=0.0, end_time=5.0 + ) + result = stats.to_dict() + + assert result["name"] == "test" + assert result["cost"] == 0.5 + assert result["tokens"] == 1000 + assert result["call_count"] == 5 + assert result["duration_seconds"] == 5.0 + assert "external_duration_seconds" in result + + +class TestAsNumber: + """Tests for _as_number helper.""" + + def test_none_returns_zero(self): + assert _as_number(None) == 0.0 + + def test_bool_returns_zero(self): + assert _as_number(True) == 0.0 + assert _as_number(False) == 0.0 + + def test_int_returns_float(self): + assert _as_number(42) == 42.0 + + def test_float_returns_float(self): + assert _as_number(3.14) == 3.14 + + def test_numeric_string_returns_float(self): + assert _as_number("10.5") == 10.5 + + def test_invalid_string_returns_zero(self): + assert _as_number("not a number") == 0.0 + + def test_list_returns_zero(self): + assert _as_number([1, 2, 3]) == 0.0 + + +class TestGetHistoryEntries: + """Tests for _get_history_entries helper.""" + + def test_returns_empty_list_when_no_lm(self): + # When LM is None, _get_history_entries should return empty list + result = _get_history_entries(None) + assert result == [] + + def test_returns_empty_list_when_no_history_attr(self): + mock_lm = MagicMock() + del mock_lm.history + result = _get_history_entries(mock_lm) + assert result == [] + + def test_returns_history_for_provided_lm(self): + """Test _get_history_entries with explicitly provided LM.""" + mock_history = [{"uuid": "test-uuid", "cost": 0.5}] + mock_lm = MagicMock() + mock_lm.history = mock_history + result = _get_history_entries(mock_lm) + assert result == mock_history + + def test_returns_empty_list_for_provided_lm_no_history(self): + """Test _get_history_entries with LM that has no history attribute.""" + mock_lm = MagicMock() + del mock_lm.history + result = _get_history_entries(mock_lm) + assert result == [] + + +class TestGetHistoryUuids: + """Tests for _get_history_uuids helper.""" + + def test_returns_set_of_uuids_for_provided_lm(self): + """Test _get_history_uuids with explicitly provided LM.""" + mock_history = [ + {"uuid": "uuid-1", "cost": 0.5}, + {"uuid": "uuid-2", "cost": 0.3}, + ] + mock_lm = MagicMock() + mock_lm.history = mock_history + result = _get_history_uuids(mock_lm) + assert result == {"uuid-1", "uuid-2"} + + +class TestCalculateCostsFromEntries: + """Tests for _calculate_costs_from_entries helper.""" + + def test_calculates_costs_and_tokens(self): + entries = [ + { + "uuid": "uuid-1", + "cost": 0.5, + "usage": {"prompt_tokens": 100, "completion_tokens": 50}, + }, + { + "uuid": "uuid-2", + "cost": 0.3, + "usage": {"prompt_tokens": 200, "completion_tokens": 100}, + }, + ] + exclude = set() + + cost, tokens, calls, input_tokens, output_tokens, input_cost, output_cost = _calculate_costs_from_entries(entries, exclude) + + assert cost == 0.8 + assert tokens == 450 + assert calls == 2 + assert input_tokens == 300 + assert output_tokens == 150 + assert input_cost + output_cost == cost # Costs should sum correctly + + def test_excludes_specified_uuids(self): + entries = [ + {"uuid": "uuid-1", "cost": 0.5, "usage": {"prompt_tokens": 100}}, + {"uuid": "uuid-2", "cost": 0.3, "usage": {"prompt_tokens": 200}}, + ] + exclude = {"uuid-1"} + + cost, tokens, calls, input_tokens, output_tokens, input_cost, output_cost = _calculate_costs_from_entries(entries, exclude) + + assert cost == 0.3 + assert tokens == 200 + assert calls == 1 + assert input_tokens == 200 # Only uuid-2's tokens + assert output_tokens == 0 + + def test_handles_non_dict_entries(self): + entries = ["not a dict", {"uuid": "uuid-1", "cost": 0.5, "usage": {}}] + exclude = set() + + cost, tokens, calls, input_tokens, output_tokens, input_cost, output_cost = _calculate_costs_from_entries(entries, exclude) + + assert cost == 0.5 + assert calls == 1 + + def test_handles_missing_usage(self): + entries = [ + {"uuid": "uuid-1", "cost": 0.5}, # No usage + ] + exclude = set() + + cost, tokens, calls, input_tokens, output_tokens, input_cost, output_cost = _calculate_costs_from_entries(entries, exclude) + + assert cost == 0.5 + assert tokens == 0 + assert calls == 1 + assert input_tokens == 0 + assert output_tokens == 0 + + def test_skips_cache_hit_entries(self): + """Entries with cache_hit=True should be skipped.""" + entries = [ + {"uuid": "uuid-1", "cost": 0.5, "usage": {"prompt_tokens": 100}, "response": {"cache_hit": True}}, + {"uuid": "uuid-2", "cost": 0.3, "usage": {"prompt_tokens": 200}, "response": {"cache_hit": False}}, + ] + exclude = set() + + cost, tokens, calls, input_tokens, output_tokens, input_cost, output_cost = _calculate_costs_from_entries(entries, exclude) + + assert cost == 0.3 + assert tokens == 200 + assert calls == 1 + assert input_tokens == 200 + +class TestCostTracker: + """Tests for CostTracker class.""" + + def test_reset_clears_all_stats(self): + tracker = CostTracker() + tracker.add_external_call("test", 1.0, 100) + tracker.reset() + + assert tracker.total_cost == 0.0 + assert tracker.total_tokens == 0 + assert tracker.call_count == 0 + + def test_start_signature_creates_new_entry(self): + tracker = CostTracker() + tracker.start_signature("test_sig") + + stats = tracker.get_signature_stats("test_sig") + assert stats is not None + assert stats.name == "test_sig" + assert stats.start_time is not None + + def test_end_signature_updates_stats(self): + tracker = CostTracker() + tracker.start_signature("test_sig") + tracker.end_signature("test_sig", 0.5, 100, 2, input_tokens=80, output_tokens=20, input_cost=0.3, output_cost=0.2) + + stats = tracker.get_signature_stats("test_sig") + assert stats.cost == 0.5 + assert stats.tokens == 100 + assert stats.call_count == 2 + assert stats.end_time is not None + assert stats.input_tokens == 80 + assert stats.output_tokens == 20 + assert stats.input_cost == 0.3 + assert stats.output_cost == 0.2 + + def test_end_signature_accumulates_multiple_calls(self): + tracker = CostTracker() + tracker.start_signature("test_sig") + tracker.end_signature("test_sig", 0.5, 100, 2, input_tokens=80, output_tokens=20, input_cost=0.3, output_cost=0.2) + tracker.end_signature("test_sig", 0.3, 50, 1, input_tokens=30, output_tokens=20, input_cost=0.2, output_cost=0.1) + + stats = tracker.get_signature_stats("test_sig") + assert stats.cost == 0.8 + assert stats.tokens == 150 + assert stats.call_count == 3 + assert stats.input_tokens == 110 # 80 + 30 + assert stats.output_tokens == 40 # 20 + 20 + assert stats.input_cost == pytest.approx(0.5) # 0.3 + 0.2 + assert stats.output_cost == pytest.approx(0.3) # 0.2 + 0.1 + + def test_total_cost_sums_all_signatures(self): + tracker = CostTracker() + tracker.add_external_call("sig1", 0.5, 100) + tracker.add_external_call("sig2", 0.3, 50) + + assert tracker.total_cost == 0.8 + + def test_total_tokens_sums_all_signatures(self): + tracker = CostTracker() + tracker.add_external_call("sig1", 0.5, 100) + tracker.add_external_call("sig2", 0.3, 50) + + assert tracker.total_tokens == 150 + + def test_call_count_sums_all_signatures(self): + tracker = CostTracker() + tracker.add_external_call("sig1", 0.5, 100, 2) + tracker.add_external_call("sig2", 0.3, 50, 3) + + assert tracker.call_count == 5 + + def test_get_signature_stats_returns_none_for_unknown(self): + tracker = CostTracker() + result = tracker.get_signature_stats("unknown") + assert result is None + + def test_get_all_signature_stats_returns_copy(self): + tracker = CostTracker() + tracker.add_external_call("test", 0.5, 100) + + stats1 = tracker.get_all_signature_stats() + tracker.add_external_call("test", 0.3, 50) # Modify after copy + stats2 = tracker.get_all_signature_stats() + + assert stats1["test"].cost == 0.5 # Original copy unchanged + assert stats2["test"].cost == 0.8 # New copy reflects updates + + +class TestCostTrackerAddExternalCall: + """Tests for CostTracker.add_external_call method.""" + + def test_add_external_call_creates_new_entry(self): + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100, 1, input_tokens=100, output_tokens=0, input_cost=0.5, output_cost=0.0) + + stats = tracker.get_signature_stats("memory_retain") + assert stats is not None + assert stats.name == "memory_retain" + assert stats.cost == 0.5 + assert stats.tokens == 100 + assert stats.call_count == 1 + assert stats.input_tokens == 100 + assert stats.output_tokens == 0 + assert stats.input_cost == 0.5 + assert stats.output_cost == 0.0 + + def test_add_external_call_accumulates_existing_entry(self): + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100, 1, input_tokens=100, output_tokens=0, input_cost=0.5, output_cost=0.0) + tracker.add_external_call("memory_retain", 0.3, 50, 2, input_tokens=30, output_tokens=20, input_cost=0.2, output_cost=0.1) + + stats = tracker.get_signature_stats("memory_retain") + assert stats.cost == 0.8 + assert stats.tokens == 150 + assert stats.call_count == 3 + assert stats.input_tokens == 130 # 100 + 30 + assert stats.output_tokens == 20 # 0 + 20 + assert stats.input_cost == 0.7 # 0.5 + 0.2 + assert stats.output_cost == 0.1 # 0.0 + 0.1 + + def test_add_external_call_does_not_touch_start_end_time(self): + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100) + + stats = tracker.get_signature_stats("memory_retain") + assert stats.start_time is None + assert stats.end_time is None + + def test_add_external_call_default_calls_is_one(self): + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100) # Uses defaults for new fields + + stats = tracker.get_signature_stats("memory_retain") + assert stats.call_count == 1 + # Default values for new fields + assert stats.input_tokens == 0 + assert stats.output_tokens == 0 + assert stats.input_cost == 0.0 + assert stats.output_cost == 0.0 + + def test_add_external_call_is_thread_safe(self): + """Concurrent add_external_call calls should all be counted.""" + tracker = CostTracker() + errors = [] + + def add_call(n): + try: + tracker.add_external_call("memory_retain", 0.1, 10, 1, input_tokens=8, output_tokens=2, input_cost=0.08, output_cost=0.02) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=add_call, args=(i,)) for i in range(100)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors + stats = tracker.get_signature_stats("memory_retain") + assert stats.cost == pytest.approx(10.0, rel=0.01) + assert stats.tokens == 1000 + assert stats.call_count == 100 + assert stats.input_tokens == 800 + assert stats.output_tokens == 200 + assert stats.input_cost == pytest.approx(8.0, rel=0.01) + assert stats.output_cost == pytest.approx(2.0, rel=0.01) + + def test_add_external_call_accumulates_duration(self): + """add_external_call with duration accumulates external_duration_seconds.""" + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100, duration=1.5) + tracker.add_external_call("memory_retain", 0.3, 50, duration=2.5) + + stats = tracker.get_signature_stats("memory_retain") + assert stats.external_duration_seconds == 4.0 # 1.5 + 2.5 + assert stats.duration_seconds == 4.0 # No start_time, so just external + assert stats.start_time is None # Should not touch start_time + assert stats.end_time is None # Should not touch end_time + + def test_add_external_call_duration_default_is_zero(self): + """add_external_call without duration defaults to 0.0.""" + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100) + + stats = tracker.get_signature_stats("memory_retain") + assert stats.external_duration_seconds == 0.0 + assert stats.duration_seconds == 0.0 + + def test_get_all_signature_stats_preserves_external_duration(self): + """get_all_signature_stats copy preserves external_duration_seconds.""" + tracker = CostTracker() + tracker.add_external_call("memory_retain", 0.5, 100, duration=2.0) + + all_stats = tracker.get_all_signature_stats() + assert all_stats["memory_retain"].external_duration_seconds == 2.0 + assert all_stats["memory_retain"].duration_seconds == 2.0 + + +class TestSignatureContext: + """Tests for SignatureContext context manager.""" + + @pytest.fixture + def mock_lm_scope(self): + """Create a mock LMScope with main and extraction LMs.""" + with patch("codespy.agents.dspy_config.lm_context") as mock: + from codespy.agents.dspy_config import LMScope + + mock_lm = MagicMock() + mock_lm.history = [] + mock_extraction_lm = MagicMock() + mock_extraction_lm.history = [] + + scope = MagicMock() + scope.lm = mock_lm + scope.extraction_lm = mock_extraction_lm + scope.__enter__ = MagicMock(return_value=scope) + scope.__exit__ = MagicMock(return_value=None) + mock.return_value = scope + yield mock + + def test_enter_applies_lm_and_starts_tracking(self, mock_lm_scope): + tracker = CostTracker() + + with SignatureContext("test_sig", tracker): + pass + + stats = tracker.get_signature_stats("test_sig") + assert stats is not None + + def test_exit_calculates_costs(self, mock_lm_scope): + tracker = CostTracker() + mock_history = [ + {"uuid": "new-uuid", "cost": 0.5, "usage": {"prompt_tokens": 100}}, + ] + + # Set history on both LMs + scope = mock_lm_scope.return_value + scope.lm.history = mock_history + scope.extraction_lm.history = [] + + with patch.object(tracker, "end_signature") as mock_end: + with SignatureContext("test_sig", tracker): + pass + + mock_end.assert_called_once() + args = mock_end.call_args + assert args[0][0] == "test_sig" + + def test_exit_always_releases_lm_context(self, mock_lm_scope): + tracker = CostTracker() + + try: + with SignatureContext("test_sig", tracker): + raise ValueError("Test error") + except ValueError: + pass + + # lm_context.__exit__ should have been called + assert mock_lm_scope.return_value.__exit__.called + + + + +class TestGetCostTracker: + """Tests for get_cost_tracker function.""" + + def test_returns_same_instance(self): + tracker1 = get_cost_tracker() + tracker2 = get_cost_tracker() + assert tracker1 is tracker2 + + def test_returns_cost_tracker_instance(self): + tracker = get_cost_tracker() + assert isinstance(tracker, CostTracker) diff --git a/tests/test_doc_extractor.py b/tests/test_doc_extractor.py new file mode 100644 index 0000000..a0cef55 --- /dev/null +++ b/tests/test_doc_extractor.py @@ -0,0 +1,280 @@ +"""Tests for the documentation extractor module.""" + +import tempfile +from pathlib import Path + +import pytest + +from codespy.agents.review.doc.doc_extractor import _trim_changelog, extract_documentation + + +class TestTrimChangelog: + """Tests for _trim_changelog function.""" + + def test_keeps_unreleased_plus_two_releases(self): + """Trim keeps [Unreleased] + two release sections.""" + content = """# Changelog + +## [Unreleased] + +### Added +- New feature + +## [1.0.2] - 2024-01-15 + +### Fixed +- Bug fix + +## [1.0.1] - 2024-01-10 + +### Fixed +- Another bug + +## [1.0.0] - 2024-01-01 + +### Added +- Initial release +""" + result = _trim_changelog(content) + assert "[Unreleased]" in result + assert "[1.0.2]" in result + assert "[1.0.1]" in result + assert "[1.0.0]" not in result + assert "[older entries omitted]" in result + + def test_no_trim_with_two_headings(self): + """No trim when fewer than 3 headings.""" + content = """# Changelog + +## [1.0.0] - 2024-01-01 + +### Added +- Initial release +""" + result = _trim_changelog(content) + assert result == content + assert "[older entries omitted]" not in result + + def test_no_trim_with_one_heading(self): + """No trim with only 1 heading.""" + content = """# Changelog + +## [Unreleased] + +### Added +- Feature +""" + result = _trim_changelog(content) + assert result == content + + def test_no_trim_with_no_headings(self): + """No trim with no headings.""" + content = """# Changelog + +Some text without proper headings. +""" + result = _trim_changelog(content) + assert result == content + + def test_exactly_three_headings(self): + """Trim at exactly 3 headings.""" + content = """# Changelog + +## [Unreleased] + +### Added +- New + +## [1.0.1] + +### Fixed +- Fix + +## [1.0.0] + +### Added +- Initial +""" + result = _trim_changelog(content) + assert "[Unreleased]" in result + assert "[1.0.1]" in result + assert "[1.0.0]" not in result + assert result.endswith("\n[older entries omitted]") + + def test_preserves_preamble(self): + """Preamble before first heading is preserved.""" + content = """# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog]. + +## [Unreleased] + +### Added +- Feature + +## [1.0.0] + +### Added +- Initial + +## [0.9.0] + +### Added +- Beta +""" + result = _trim_changelog(content) + assert "# Changelog" in result + assert "All notable changes" in result + assert "Keep a Changelog" in result + assert "[Unreleased]" in result + assert "[1.0.0]" in result + assert "[0.9.0]" not in result + + +class TestExtractDocumentation: + """Tests for extract_documentation function.""" + + def test_extracts_readme(self): + """README files are extracted.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + (root / "README.md").write_text("# README\n\nHello world") + + result = extract_documentation(root) + + assert "=== README.md ===" in result + assert "# README" in result + assert "Hello world" in result + + def test_extracts_changelog_and_trims(self): + """CHANGELOG files are extracted and trimmed.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + changelog_content = """# Changelog + +## [Unreleased] + +### Added +- New + +## [1.0.1] + +### Fixed +- Fix + +## [1.0.0] + +### Added +- Initial + +## [0.9.0] + +### Added +- Old +""" + (root / "CHANGELOG.md").write_text(changelog_content) + + result = extract_documentation(root) + + assert "=== CHANGELOG.md ===" in result + assert "[Unreleased]" in result + assert "[1.0.1]" in result + assert "[1.0.0]" in result + assert "[0.9.0]" not in result + assert "[older entries omitted]" in result + + def test_non_changelog_not_trimmed(self): + """Non-CHANGELOG files are not trimmed.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + content = """# README + +## Section 1 +Text 1 + +## Section 2 +Text 2 + +## Section 3 +Text 3 +""" + (root / "README.md").write_text(content) + + result = extract_documentation(root) + + # README should NOT be trimmed - all sections preserved + assert "Section 1" in result + assert "Section 2" in result + assert "Section 3" in result + assert "[older entries omitted]" not in result + + def test_changelog_case_insensitive(self): + """CHANGELOG matching is case-insensitive.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + changelog_content = """# Changelog + +## [Unreleased] +### Added +- New + +## [1.0.0] +### Added +- Initial + +## [0.9.0] +### Added +- Old +""" + (root / "changelog").write_text(changelog_content) + + result = extract_documentation(root) + + # Should be trimmed because filename matches case-insensitively + assert "[older entries omitted]" in result + assert "[0.9.0]" not in result + + def test_header_unchanged_when_trimmed(self): + """The `=== path ===` header is unchanged when trimming.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + changelog_content = """# Changelog + +## [Unreleased] + +### Added +- New + +## [1.0.0] + +### Added +- Initial + +## [0.9.0] + +### Added +- Old +""" + (root / "CHANGELOG.md").write_text(changelog_content) + + result = extract_documentation(root) + + # Header should be exactly as expected for filename detection + assert "=== CHANGELOG.md ===" in result + + def test_returns_empty_for_no_docs(self): + """Returns empty string when no documentation exists.""" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + (root / "main.py").write_text("print('hello')") + + result = extract_documentation(root) + + assert result == "" + + def test_returns_empty_for_nonexistent_path(self): + """Returns empty string when scope root does not exist.""" + result = extract_documentation(Path("/nonexistent/path")) + assert result == "" diff --git a/tests/test_episode_saves.py b/tests/test_episode_saves.py new file mode 100644 index 0000000..d444ef8 --- /dev/null +++ b/tests/test_episode_saves.py @@ -0,0 +1,200 @@ +"""Tests for episode save threading and at-exit behavior. + +These tests verify that background episode saves complete before the process +exits, preventing "cannot schedule new futures after shutdown" errors from +Cerebral consolidation embedding calls. +""" + +from __future__ import annotations + +import asyncio +import subprocess +import sys +import threading +import time +from concurrent.futures import ThreadPoolExecutor + +import pytest + +from codespy.agents.memory.hippocampus.episode import ( + join_episode_saves, + submit_episode_save, +) + + +class TestJoinEpisodeSaves: + """Tests for join_episode_saves behavior.""" + + def test_join_no_timeout_waits_for_long_thread(self) -> None: + """join_episode_saves with no timeout should wait for slow threads.""" + result: list[str] = [] + + def slow_save() -> None: + # Sleep longer than the old default timeout (120s) + time.sleep(0.3) + result.append("done") + + submit_episode_save(slow_save, name="slow-save") + join_episode_saves(timeout_per_thread=None) + + assert result == ["done"] + + def test_join_with_timeout_returns_early(self) -> None: + """join_episode_saves with explicit timeout should return early and warn.""" + result: list[str] = [] + + def slow_save() -> None: + time.sleep(1.0) # Longer than our timeout + result.append("done") + + submit_episode_save(slow_save, name="slow-timeout-save") + + # Should return immediately after timeout with warning + join_episode_saves(timeout_per_thread=0.1) + + # Result should not be set yet (thread still running) + assert result == [] + + def test_join_catches_saves_submitted_during_join(self) -> None: + """Saves submitted during join should also be joined.""" + results: list[str] = [] + + def first_save() -> None: + # This save submits another save + def second_save() -> None: + time.sleep(0.05) + results.append("second") + + submit_episode_save(second_save, name="second-save") + time.sleep(0.1) + results.append("first") + + submit_episode_save(first_save, name="first-save") + join_episode_saves(timeout_per_thread=None) + + assert "first" in results + assert "second" in results + + +class TestSubprocessRegression: + """Subprocess regression test for the executor shutdown race condition.""" + + def test_executor_not_shutdown_before_save_completes(self) -> None: + """Verify that ThreadPoolExecutor works during episode save at exit. + + This test runs in a subprocess to simulate the actual interpreter exit + scenario. It creates a daemon event loop thread (like Cerebral does), + submits an episode save that uses run_in_executor, and exits without + explicitly joining. The subprocess should complete successfully without + raising "cannot schedule new futures after shutdown". + """ + test_code = ''' +import asyncio +import sys +import threading +import time + +# Import episode module to trigger the at-exit hook registration +from codespy.agents.memory.hippocampus.episode import submit_episode_save + +# Create a daemon event loop thread like Cerebral does +loop = asyncio.new_event_loop() + +def run_loop(): + asyncio.set_event_loop(loop) + loop.run_forever() + +loop_thread = threading.Thread(target=run_loop, daemon=True) +loop_thread.start() + +result = [] + +def save_target(): + async def async_task(): + # This uses the loop's default executor (ThreadPoolExecutor) + # This is what litellm.aembedding does internally + return await loop.run_in_executor(None, lambda: 42) + + # Submit to the loop and wait for result + future = asyncio.run_coroutine_threadsafe(async_task(), loop) + val = future.result(timeout=5.0) + result.append(val) + print("OK", flush=True) + +submit_episode_save(save_target, name="test-save") + +# Main thread exits WITHOUT calling join_episode_saves() +# The at-exit hook should ensure the save completes +''' + # Run the test code in a subprocess + proc = subprocess.run( + [sys.executable, "-c", test_code], + capture_output=True, + text=True, + timeout=30, # Overall timeout for the subprocess + ) + + stdout = proc.stdout + stderr = proc.stderr + + # Should succeed + assert proc.returncode == 0, ( + f"Subprocess failed with return code {proc.returncode}.\n" + f"stdout: {stdout}\nstderr: {stderr}" + ) + + # Should print OK + assert "OK" in stdout, f"Expected 'OK' in stdout, got: {stdout}" + + # Should NOT have the executor shutdown error + assert "cannot schedule new futures after shutdown" not in stderr, ( + f"Got executor shutdown error: {stderr}" + ) + assert "cannot schedule new futures after shutdown" not in stdout, ( + f"Got executor shutdown error in stdout: {stdout}" + ) + + +class TestThreadPoolExecutorDuringSave: + """Tests verifying ThreadPoolExecutor behavior during episode saves.""" + + def test_loop_default_executor_works_in_save(self) -> None: + """ThreadPoolExecutor calls should work inside episode save threads.""" + results: list[int] = [] + + # Create an event loop in a daemon thread (simulating Cerebral's loop) + loop = asyncio.new_event_loop() + + def run_loop() -> None: + asyncio.set_event_loop(loop) + # Set up a default executor + executor = ThreadPoolExecutor(max_workers=2) + loop.set_default_executor(executor) + loop.run_forever() + # Cleanup + executor.shutdown(wait=True) + + loop_thread = threading.Thread(target=run_loop, daemon=True) + loop_thread.start() + + def save_with_executor() -> None: + async def async_work() -> int: + # This should work - uses the loop's default executor + return await loop.run_in_executor(None, lambda: 42) + + future = asyncio.run_coroutine_threadsafe(async_work(), loop) + val = future.result(timeout=5.0) + results.append(val) + + submit_episode_save(save_with_executor, name="executor-save") + join_episode_saves(timeout_per_thread=None) + + # Stop the loop + loop.call_soon_threadsafe(loop.stop) + loop_thread.join(timeout=2.0) + + assert results == [42] + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_hippocampus.py b/tests/test_hippocampus.py index ddb8a62..2f0c1ae 100644 --- a/tests/test_hippocampus.py +++ b/tests/test_hippocampus.py @@ -19,7 +19,6 @@ Operation, OpType, Topic, - inject_context_memory, ) from codespy.agents.memory.hippocampus.episode import Episode from codespy.agents.memory.postgres import EpisodeStore @@ -337,8 +336,8 @@ def forward(self, **kwargs): topics=[Topic(id="test/topic", type="project_scope", description="Test topic")], ) - # Make a call (MockAgent accepts any kwargs, so no inject needed for this test) - result = agent(context_memory=hippo.context_memory) + # Agents no longer receive context_memory as input + result = agent() hippo.observe(result) # End episode with the store @@ -470,9 +469,8 @@ def test_delete_creates_tombstone(self, episode_store): topic_ids=[topic_id], ) - # Should have no observations (deleted) - assert loaded_ctx is not None - assert len(loaded_ctx.context_understanding) == 0 + # When all observations are deleted, load_context returns None + assert loaded_ctx is None class TestRecordMutations: @@ -596,3 +594,575 @@ def test_neutral_preserves_existing(self): hip.scores = {"a": 5} hip._update_observation_scores({"a": ObservationTag.NEUTRAL}) assert hip.scores == {"a": 5} + + +def _stub_reflection(hippo, operations=None): + """Replace the Distiller/Cartographer with no-LLM stubs.""" + import dspy + + hippo.distill = lambda **kw: dspy.Prediction( + diagnosis="", observation_tags={}, cache_candidates=[] + ) + hippo.cartograph = lambda **kw: dspy.Prediction( + justification="", operations=list(operations or []) + ) + + +class TestEviction: + """Eviction of prior observations records an EVICT mutation (not DELETE).""" + + TOPIC = "owner/repo/evict-unit" + + def _prior(self, n: int, size: int = 400) -> ContextMemory: + return ContextMemory( + topics=[Topic(id=self.TOPIC, type="project_scope", description="t")], + context_understanding=[ + Observation( + id=f"cu-prior{i}", content=f"{i} " + "x" * size, topic_ids=[self.TOPIC] + ) + for i in range(n) + ], + ) + + def test_evicted_prior_records_evict_mutation(self): + from codespy.agents.memory.hippocampus import MemoryBudget, MutationType + + prior = self._prior(4) + hippo = Hippocampus( + task_name="evict_test", + budget=MemoryBudget(max_hippocampus_tokens=300), + initial_memory=prior, + ) + _stub_reflection(hippo) + hippo._distill("trajectory", "question") + + survivors = hippo.cmem.ids() + evicted_ids = {o.id for o in prior.all_observations()} - survivors + assert evicted_ids, "budget should force eviction" + + evicts = [m for m in hippo._mutations if m.type == MutationType.EVICT] + assert {m.observation_id for m in evicts} == evicted_ids + assert not any(m.type == MutationType.DELETE for m in hippo._mutations) + prior_by_id = {o.id: o for o in prior.all_observations()} + for m in evicts: + assert m.content is None + assert m.section == "context_understanding" + assert m.previous_content == prior_by_id[m.observation_id].content + assert m.topic_ids == [self.TOPIC] + assert m.step == 0 + + def test_no_eviction_records_nothing(self): + from codespy.agents.memory.hippocampus import MemoryBudget + + hippo = Hippocampus( + task_name="evict_test", + budget=MemoryBudget(max_hippocampus_tokens=100_000), + initial_memory=self._prior(2), + ) + _stub_reflection(hippo) + hippo._distill("trajectory", "question") + assert hippo._mutations == [] + + def test_evicted_own_add_records_evict(self): + from codespy.agents.memory.hippocampus import MemoryBudget, MutationType + + hippo = Hippocampus( + task_name="evict_test", + budget=MemoryBudget(max_hippocampus_tokens=200), + topics=[Topic(id=self.TOPIC, type="project_scope", description="t")], + ) + ops = [ + Operation(type=OpType.ADD, section="context_understanding", content=f"{i} " + "y" * 400) + for i in range(3) + ] + _stub_reflection(hippo, ops) + hippo._distill("trajectory", "question") + + assert len(hippo.cmem.all_observations()) < 3, "budget should force eviction" + # Now records EVICT for own ADDs too, with ADD's content as previous_content + evicts = [m for m in hippo._mutations if m.type == MutationType.EVICT] + assert len(evicts) > 0, "should record EVICT for evicted own ADDs" + for m in evicts: + assert m.content is None + assert m.previous_content is not None + assert m.previous_content.startswith("0 ") or m.previous_content.startswith("1 ") or m.previous_content.startswith("2 ") + + def test_episode_keeps_mutations_after_end_episode(self): + from codespy.agents.memory.hippocampus import MemoryBudget, MutationType + + hippo = Hippocampus( + task_name="evict_test", + budget=MemoryBudget(max_hippocampus_tokens=300), + initial_memory=self._prior(4), + ) + _stub_reflection(hippo) + hippo.observe("trajectory") + hippo.end_episode() + assert hippo.episode is not None + assert any(m.type == MutationType.EVICT for m in hippo.episode.mutations) + assert hippo._mutations == [] + + def test_cartographer_cannot_emit_evict(self): + """EVICT is a MutationType only; Operation (Cartographer output) rejects it.""" + from pydantic import ValidationError + + with pytest.raises(ValidationError): + Operation(type="EVICT", observation_id="cu-x") + + +class TestEvictTombstone: + """Store round-trip: EVICT writes a tombstone version and hides the observation.""" + + def _row_versions(self, store, obs_id): + from psycopg.rows import dict_row + + with store._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + cur.execute( + "SELECT version, op_type, content, previous_content FROM observations " + "WHERE bank_id = %s AND id = %s ORDER BY version", + (store.bank_id, obs_id), + ) + return cur.fetchall() + + def test_evict_tombstone_round_trip(self, episode_store): + import uuid + + from codespy.agents.memory.hippocampus import MutationType + + topic_id = f"owner/repo/evict-{uuid.uuid4().hex[:8]}" + topic = Topic(id=topic_id, type="project_scope", description="t") + keep_id, evict_id = f"cu-keep{uuid.uuid4().hex}", f"cu-evict{uuid.uuid4().hex}" + + ctx1 = ContextMemory( + topics=[topic], + context_understanding=[ + Observation(id=keep_id, content="keep me", topic_ids=[topic_id]), + Observation(id=evict_id, content="evict me", topic_ids=[topic_id]), + ], + ) + episode_store.save_episode(Episode( + id=uuid.uuid4(), task="code_review", module="m", question="q1", + context_memory=ctx1, timestamp=datetime.now(UTC), run_id="r1", + mutations=[ + Mutation(step=0, type=MutationType.ADD, observation_id=oid, + section="context_understanding", content=c, topic_ids=[topic_id]) + for oid, c in ((keep_id, "keep me"), (evict_id, "evict me")) + ], + artifacts={}, + )) + + # Episode 2: the prior observation was evicted (not in context_memory) + ctx2 = ContextMemory( + topics=[topic], + context_understanding=[Observation(id=keep_id, content="keep me", topic_ids=[topic_id])], + ) + episode_store.save_episode(Episode( + id=uuid.uuid4(), task="code_review", module="m", question="q2", + context_memory=ctx2, timestamp=datetime.now(UTC), run_id="r2", + mutations=[ + Mutation(step=0, type=MutationType.EVICT, observation_id=evict_id, + section="context_understanding", content=None, + previous_content="evict me", topic_ids=[topic_id]) + ], + artifacts={}, + )) + + rows = self._row_versions(episode_store, evict_id) + assert [(r["version"], r["op_type"]) for r in rows] == [(1, "ADD"), (2, "EVICT")] + assert rows[1]["content"] is None + assert rows[1]["previous_content"] == "evict me" + + loaded = episode_store.load_context(task="code_review", topic_ids=[topic_id]) + assert loaded is not None + assert loaded.ids() == {keep_id} + + def test_add_then_evict_same_run_no_orphan_row(self, episode_store): + """ADD then EVICT of a new id in same run → no row in observations.""" + import uuid + + from codespy.agents.memory.hippocampus import MutationType + + topic_id = f"owner/repo/add-evict-{uuid.uuid4().hex[:8]}" + topic = Topic(id=topic_id, type="project_scope", description="t") + new_id = f"cu-new{uuid.uuid4().hex}" + + # Single episode: ADD then EVICT the same observation + ctx = ContextMemory( + topics=[topic], + context_understanding=[], # Evicted, so not in context + ) + episode_store.save_episode(Episode( + id=uuid.uuid4(), task="code_review", module="m", question="q", + context_memory=ctx, timestamp=datetime.now(UTC), run_id="r", + mutations=[ + Mutation(step=0, type=MutationType.ADD, observation_id=new_id, + section="context_understanding", content="new content", topic_ids=[topic_id]), + Mutation(step=1, type=MutationType.EVICT, observation_id=new_id, + section="context_understanding", content=None, + previous_content="new content", topic_ids=[topic_id]), + ], + artifacts={}, + )) + + # No rows for this observation (ADD was never persisted, EVICT skipped tombstone) + rows = self._row_versions(episode_store, new_id) + assert rows == [], f"expected no rows for ADD→EVICT in same run, got {rows}" + + def test_load_orders_oldest_first_so_eviction_drops_oldest(self, episode_store): + """Load order follows episode time, not id; evict() then drops the oldest.""" + import uuid + from datetime import timedelta + + from codespy.agents.memory.hippocampus import MemoryBudget, MutationType + + suffix = uuid.uuid4().hex + topic_id = f"owner/repo/order-{suffix[:8]}" + topic = Topic(id=topic_id, type="project_scope", description="t") + # Id order is the reverse of time order: the oldest has the largest id. + ids_oldest_first = [f"cu-z{suffix}", f"cu-m{suffix}", f"cu-a{suffix}"] + now = datetime.now(UTC) + for age, oid in zip((3, 2, 1), ids_oldest_first, strict=True): + content = f"{oid} " + "x" * 400 + episode_store.save_episode(Episode( + id=uuid.uuid4(), task="code_review", module="m", question="q", + context_memory=ContextMemory( + topics=[topic], + context_understanding=[ + Observation(id=oid, content=content, topic_ids=[topic_id]) + ], + ), + timestamp=now - timedelta(days=age), run_id=f"r-{age}", + mutations=[ + Mutation(step=0, type=MutationType.ADD, observation_id=oid, + section="context_understanding", content=content, + topic_ids=[topic_id]) + ], + artifacts={}, + )) + + loaded = episode_store.load_context(task="code_review", topic_ids=[topic_id]) + assert loaded is not None + assert [o.id for o in loaded.context_understanding] == ids_oldest_first + + # Budget fits only the newest observation: the two oldest are evicted. + hippo = Hippocampus( + task_name="code_review", + budget=MemoryBudget(max_hippocampus_tokens=250), + initial_memory=loaded, + ) + _stub_reflection(hippo) + hippo._distill("trajectory", "question") + assert hippo.cmem.ids() == {ids_oldest_first[-1]} + assert [m.observation_id for m in hippo._mutations] == ids_oldest_first[:2] + + +class TestLoadContextTombstones: + """load_context never fails on tombstones and uses bindings of the loaded version.""" + + @staticmethod + def _save_add(store, topic, oid, content, topic_ids=None): + import uuid + + from codespy.agents.memory.hippocampus import MutationType + + ep_id = uuid.uuid4() + store.save_episode(Episode( + id=ep_id, task="code_review", module="m", question="q", + context_memory=ContextMemory( + topics=[topic], + context_understanding=[ + Observation(id=oid, content=content, topic_ids=topic_ids or [topic.id]) + ], + ), + timestamp=datetime.now(UTC), run_id="r", + mutations=[ + Mutation(step=0, type=MutationType.ADD, observation_id=oid, + section="context_understanding", content=content, + topic_ids=topic_ids or [topic.id]) + ], + artifacts={}, + )) + return ep_id + + def test_unknown_tombstone_type_is_skipped_not_fatal(self, episode_store): + """A NULL-content latest version of an unknown op_type hides only that observation.""" + import uuid + + suffix = uuid.uuid4().hex + topic = Topic(id=f"owner/repo/tomb-{suffix[:8]}", type="project_scope", description="t") + live_id, dead_id = f"cu-live{suffix}", f"cu-dead{suffix}" + self._save_add(episode_store, topic, live_id, "live") + ep_id = self._save_add(episode_store, topic, dead_id, "dead") + + with episode_store._pool.connection() as conn, conn.cursor() as cur: + cur.execute( + "INSERT INTO observations " + "(bank_id, id, version, type, content, episode_id, step, op_type, " + "previous_content, ordinal) " + "VALUES (%s, %s, 2, 'context_understanding', NULL, %s, 0, 'ARCHIVE', 'dead', 0)", + (episode_store.bank_id, dead_id, str(ep_id)), + ) + conn.commit() + + loaded = episode_store.load_context(task="code_review", topic_ids=[topic.id]) + assert loaded is not None + assert loaded.ids() == {live_id} + + def test_tombstone_types_come_from_tombstone_types(self, episode_store, monkeypatch): + """load_context reads the tombstone list from TOMBSTONE_TYPES.""" + import uuid + + from codespy.agents.memory.hippocampus import context_memory as cm + + suffix = uuid.uuid4().hex + topic = Topic(id=f"owner/repo/types-{suffix[:8]}", type="project_scope", description="t") + oid = f"cu-add{suffix}" + self._save_add(episode_store, topic, oid, "content") + + # Treat ADD as a tombstone type: the ADD-only observation must disappear. + monkeypatch.setattr( + cm, "TOMBSTONE_TYPES", frozenset(cm.TOMBSTONE_TYPES | {cm.MutationType.ADD}) + ) + assert episode_store.load_context(task="code_review", topic_ids=[topic.id]) is None + + def test_bindings_come_from_loaded_version(self, episode_store): + """After a REPLACE rebinds topics, only the latest version's bindings load.""" + import uuid + + from codespy.agents.memory.hippocampus import MutationType + + suffix = uuid.uuid4().hex + t1 = Topic(id=f"owner/repo/b1-{suffix[:8]}", type="project_scope", description="t1") + t2 = Topic(id=f"owner/repo/b2-{suffix[:8]}", type="project_scope", description="t2") + oid = f"cu-rebind{suffix}" + self._save_add(episode_store, t1, oid, "v1", topic_ids=[t1.id]) + + episode_store.save_episode(Episode( + id=uuid.uuid4(), task="code_review", module="m", question="q", + context_memory=ContextMemory( + topics=[t1, t2], + context_understanding=[Observation(id=oid, content="v2", topic_ids=[t2.id])], + ), + timestamp=datetime.now(UTC), run_id="r", + mutations=[ + Mutation(step=0, type=MutationType.REPLACE, observation_id=oid, + section="context_understanding", content="v2", + previous_content="v1", topic_ids=[t2.id]) + ], + artifacts={}, + )) + + loaded = episode_store.load_context(task="code_review", topic_ids=[t1.id]) + assert loaded is not None + obs = loaded.find_observation(oid)[1] + assert obs.content == "v2" + assert obs.topic_ids == [t2.id] + + +def _recall_records(): + from codespy.agents.memory.recall import RecallRecord + + return [ + RecallRecord( + kind="load", + query="context query", + reach="org", + reflects=5, + status="ok", + text="## Around this work\nRECALLED-SECRET-TEXT", + model="openai/gpt-4o-mini", + llm_calls=3, + input_tokens=1200, + output_tokens=300, + input_cost=0.012, + output_cost=0.006, + latency_ms=4200, + details={"briefings": 1, "remote": 2, "facets": {"context": {"count": 1}}}, + ), + RecallRecord( + kind="tool", + query="where is login", + reach="local", + status="limit", + text="recall limit reached", + details={"requested_reach": "bank", "mode": "reflect"}, + ), + ] + + +class TestEpisodeStoreRecalls: + """Prefrontal recalls are persisted to the recalls table, never distilled.""" + + def _episode(self, recalls): + import uuid + + return Episode( + id=uuid.uuid4(), + task="code_review", + module="m", + question="q", + context_memory=ContextMemory( + topics=[Topic(id="owner/repo/recalls", type="project_scope", description="t")] + ), + timestamp=datetime.now(UTC), + run_id="run-recalls", + recalls=recalls, + ) + + def _rows(self, store, episode_id): + from psycopg.rows import dict_row + + with store._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM recalls WHERE bank_id = %s AND episode_id = %s ORDER BY timestamp", + (store.bank_id, str(episode_id)), + ) + return cur.fetchall() + + def test_save_writes_recall_rows(self, episode_store): + episode = self._episode(_recall_records()) + episode_store.save_episode(episode) + + rows = self._rows(episode_store, episode.id) + assert len(rows) == 2 + load, tool = rows + assert load["kind"] == "load" and load["status"] == "ok" + assert load["text"].endswith("RECALLED-SECRET-TEXT") + assert load["model"] == "openai/gpt-4o-mini" + assert load["llm_calls"] == 3 + assert load["input_tokens"] == 1200 and load["output_tokens"] == 300 + assert load["input_cost"] == pytest.approx(0.012) + assert load["output_cost"] == pytest.approx(0.006) + assert load["latency_ms"] == 4200 + assert load["reflects"] == 5 and load["reach"] == "org" + assert load["details"]["facets"]["context"]["count"] == 1 + # New schema: run_id and task are populated from episode + assert load["run_id"] == "run-recalls" + assert load["task"] == "code_review" + assert tool["status"] == "limit" + assert tool["details"] == {"requested_reach": "bank", "mode": "reflect"} + + def test_save_is_idempotent(self, episode_store): + episode = self._episode(_recall_records()) + episode_store.save_episode(episode) + episode_store.save_episode(episode) + assert len(self._rows(episode_store, episode.id)) == 2 + + def test_episode_without_recalls(self, episode_store): + episode = self._episode([]) + episode_store.save_episode(episode) + assert self._rows(episode_store, episode.id) == [] + + def test_save_recalls_with_null_episode_id(self, episode_store): + """Run-level recalls have episode_id=NULL.""" + import uuid + from codespy.agents.memory.recall import RecallRecord + + recalls = [ + RecallRecord( + id=uuid.uuid4(), + run_id="run-level-test", + task="review", + kind="load", + timestamp=datetime.now(UTC), + query="test query", + reach="local", + status="ok", + text="test text", + model="test-model", + ), + ] + episode_store.save_recalls(recalls, run_id="run-level-test", episode_id=None, task="review") + # Query by run_id + from psycopg.rows import dict_row + with episode_store._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + cur.execute( + "SELECT * FROM recalls WHERE bank_id = %s AND run_id = %s", + (episode_store.bank_id, "run-level-test"), + ) + rows = cur.fetchall() + assert len(rows) == 1 + assert rows[0]["episode_id"] is None + assert rows[0]["task"] == "review" + + def test_legacy_table_is_dropped_and_recreated(self, episode_store): + """If recalls table exists without 'id' column, it should be dropped and recreated.""" + from psycopg.rows import dict_row + + # Manually create a legacy table (without 'id' column, with 'ordinal') + with episode_store._pool.connection() as conn: + with conn.cursor() as cur: + cur.execute("DROP TABLE IF EXISTS recalls") + cur.execute(""" + CREATE TABLE recalls ( + bank_id VARCHAR(64) NOT NULL, + episode_id UUID NOT NULL, + ordinal INT NOT NULL, + kind VARCHAR(8) NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + query TEXT NOT NULL DEFAULT '', + reach VARCHAR(8) NOT NULL, + reflects INT NOT NULL DEFAULT 0, + status VARCHAR(8) NOT NULL, + text TEXT NOT NULL DEFAULT '', + model TEXT NOT NULL DEFAULT '', + llm_calls INT NOT NULL DEFAULT 0, + input_tokens INT NOT NULL DEFAULT 0, + output_tokens INT NOT NULL DEFAULT 0, + input_cost DOUBLE PRECISION NOT NULL DEFAULT 0, + output_cost DOUBLE PRECISION NOT NULL DEFAULT 0, + latency_ms INT NOT NULL DEFAULT 0, + details JSONB NOT NULL DEFAULT '{}'::jsonb, + PRIMARY KEY (bank_id, episode_id, ordinal) + ) + """) + conn.commit() + + # Call ensure_schema which should detect and drop the legacy table + episode_store.ensure_schema() + + # Verify the new schema has 'id' column + with episode_store._pool.connection() as conn: + conn.row_factory = dict_row + with conn.cursor() as cur: + cur.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'recalls' AND table_schema = COALESCE(current_schema(), 'public') + """) + columns = {row["column_name"] for row in cur.fetchall()} + assert "id" in columns + assert "ordinal" not in columns + + +class TestHippocampusRecalls: + def test_end_episode_attaches_recalls_without_distilling_them(self): + hippo = Hippocampus(task_name="code_review", run_id="r", question="question") + hippo.observe("trajectory text") + seen: list[tuple[str, str]] = [] + hippo._distill = lambda trajectory, question: seen.append((trajectory, question)) + + recalls = _recall_records() + hippo.end_episode(recalls=recalls) + + assert hippo.episode is not None + # New schema: recalls have id (UUID) and task instead of ordinal + assert len(hippo.episode.recalls) == 2 + assert all(r.task == "" for r in hippo.episode.recalls) # Not set yet + assert all(r.id is not None for r in hippo.episode.recalls) + assert len(seen) == 1 + trajectory, question = seen[0] + assert "RECALLED-SECRET-TEXT" not in trajectory + assert "RECALLED-SECRET-TEXT" not in question + + def test_end_episode_defaults_to_no_recalls(self): + hippo = Hippocampus(task_name="code_review", run_id="r") + hippo.observe("trajectory text") + hippo._distill = lambda trajectory, question: None + hippo.end_episode() + assert hippo.episode is not None and hippo.episode.recalls == [] diff --git a/tests/test_patch_utils.py b/tests/test_patch_utils.py deleted file mode 100644 index dce0993..0000000 --- a/tests/test_patch_utils.py +++ /dev/null @@ -1,600 +0,0 @@ -"""Tests for patch_utils module.""" - -from pathlib import Path -from unittest.mock import MagicMock - -from codespy.agents.review.scope.models import ScopeResult, ScopeType -from codespy.tools.git.models import ChangedFile, FileStatus -from codespy.tools.git.patch_utils import ( - _expand_hunk_to_functions, - _merge_hunks, - _parse_hunks, - _rebuild_patch, - _should_compact_file, - compact_patch, - compact_patches, -) -from codespy.tools.parsers.treesitter import FunctionInfo - - -class TestShouldCompactFile: - """Tests for _should_compact_file function.""" - - def test_returns_true_for_code_file_with_patch(self): - file = ChangedFile( - filename="src/main.py", - status=FileStatus.MODIFIED, - patch="@@ -1,3 +1,3 @@\n-old\n+new", - ) - assert _should_compact_file(file) is True - - def test_returns_false_for_no_patch(self): - file = ChangedFile( - filename="src/main.py", - status=FileStatus.MODIFIED, - patch=None, - ) - assert _should_compact_file(file) is False - - def test_returns_false_for_deleted_file(self): - file = ChangedFile( - filename="src/main.py", - status=FileStatus.REMOVED, - patch="@@ -1,3 +0,0 @@\n-old", - ) - assert _should_compact_file(file) is False - - def test_returns_false_for_non_code_file(self): - file = ChangedFile( - filename="README.md", - status=FileStatus.MODIFIED, - patch="@@ -1,3 +1,3 @@\n-old\n+new", - ) - assert _should_compact_file(file) is False - - def test_returns_false_for_binary_file(self): - file = ChangedFile( - filename="image.png", - status=FileStatus.ADDED, - patch=None, - ) - assert _should_compact_file(file) is False - - def test_returns_false_for_lock_file(self): - file = ChangedFile( - filename="package-lock.json", - status=FileStatus.MODIFIED, - patch="@@ -1,3 +1,3 @@\n-old\n+new", - ) - assert _should_compact_file(file) is False - - -class TestParseHunks: - """Tests for _parse_hunks function.""" - - def test_parses_single_hunk(self): - patch = """@@ -1,5 +1,5 @@ - def hello(): -- print("old") -+ print("new") - pass""" - hunks = _parse_hunks(patch) - - assert len(hunks) == 1 - assert hunks[0]["old_start"] == 1 - assert hunks[0]["old_count"] == 5 - assert hunks[0]["new_start"] == 1 - assert hunks[0]["new_count"] == 5 - assert len(hunks[0]["lines"]) == 4 # Includes the context line - - def test_parses_multiple_hunks(self): - patch = """@@ -1,3 +1,3 @@ - line1 --line2 -+line2_changed - line3 -@@ -10,3 +10,3 @@ - line10 --line11 -+line11_changed - line12""" - hunks = _parse_hunks(patch) - - assert len(hunks) == 2 - assert hunks[0]["new_start"] == 1 - assert hunks[1]["new_start"] == 10 - - def test_handles_hunk_without_old_count(self): - patch = """@@ -1 +1 @@ --old -+new""" - hunks = _parse_hunks(patch) - - assert len(hunks) == 1 - assert hunks[0]["old_count"] == 1 - assert hunks[0]["new_count"] == 1 - - def test_tracks_changed_new_lines(self): - patch = """@@ -5,5 +5,6 @@ - context1 --added1 - context2 -+added2 - context3""" - hunks = _parse_hunks(patch) - - # Changed lines in new file: line 7 (added2) - deleted lines aren't in new file - # Line 5: context1 (line 5), Line 6: deleted (not in new), Line 6: context2 (line 6) - # Line 7: added2 (line 7) - assert hunks[0]["changed_new_lines"] == [7] - - def test_returns_empty_list_for_no_hunks(self): - patch = "no hunk headers here" - hunks = _parse_hunks(patch) - assert hunks == [] - - def test_handles_no_newline_at_end_marker(self): - patch = """@@ -1,3 +1,3 @@ - line1 - line2 --line3 -+line3_new -\\ No newline at end of file""" - hunks = _parse_hunks(patch) - - assert len(hunks) == 1 - # The "\ No newline" line is included in hunk lines - # Lines: line1 (context), -line3 (deleted), +line3_new (added), \ marker - assert len(hunks[0]["lines"]) == 5 - - -class TestExpandHunkToFunctions: - """Tests for _expand_hunk_to_functions function.""" - - def test_expands_to_enclosing_function(self): - hunk = { - "header": "@@ -17,7 +17,7 @@", - "old_start": 17, - "old_count": 7, - "new_start": 17, - "new_count": 7, - "lines": [" line1", "- old_line", "+ new_line", " line2"], - "changed_new_lines": [18], - } - # Function from line 10 to 30 - functions = [ - FunctionInfo( - name="test_func", - file="test.py", - line_start=10, - line_end=30, - parameters=[], - ) - ] - source_lines = [f"line {i}" for i in range(1, 35)] - - result = _expand_hunk_to_functions(hunk, functions, source_lines) - - assert result is not None - assert result["expansion_start"] == 10 - assert result["expansion_end"] == 30 - - def test_no_expansion_for_change_outside_function(self): - hunk = { - "header": "@@ -1,3 +1,4 @@", - "old_start": 1, - "old_count": 3, - "new_start": 1, - "new_count": 4, - "lines": ["+import new_module", " import os"], - "changed_new_lines": [1], - } - # Function starts at line 10 - functions = [ - FunctionInfo( - name="test_func", - file="test.py", - line_start=10, - line_end=30, - parameters=[], - ) - ] - source_lines = [f"line {i}" for i in range(1, 35)] - - result = _expand_hunk_to_functions(hunk, functions, source_lines) - - # Should return hunk unchanged (no enclosing function) - assert result == hunk - - def test_uses_innermost_function_for_nested(self): - hunk = { - "header": "@@ -15,3 +15,3 @@", - "old_start": 15, - "old_count": 3, - "new_start": 15, - "new_count": 3, - "lines": ["- old", "+ new", " pass"], - "changed_new_lines": [15], - } - # Outer function: lines 5-25, inner function: lines 12-18 - functions = [ - FunctionInfo(name="outer", file="test.py", line_start=5, line_end=25, parameters=[]), - FunctionInfo(name="inner", file="test.py", line_start=12, line_end=18, parameters=[]), - ] - source_lines = [f"line {i}" for i in range(1, 30)] - - result = _expand_hunk_to_functions(hunk, functions, source_lines) - - # Should use inner function boundaries - assert result["expansion_start"] == 12 - assert result["expansion_end"] == 18 - - def test_expands_multiple_functions_in_one_hunk(self): - hunk = { - "header": "@@ -20,10 +20,12 @@", - "old_start": 20, - "old_count": 10, - "new_start": 20, - "new_count": 12, - "lines": ["-old", "+new"], - "changed_new_lines": [22, 35], - } - functions = [ - FunctionInfo(name="func1", file="test.py", line_start=15, line_end=25, parameters=[]), - FunctionInfo(name="func2", file="test.py", line_start=30, line_end=40, parameters=[]), - ] - source_lines = [f"line {i}" for i in range(1, 50)] - - result = _expand_hunk_to_functions(hunk, functions, source_lines) - - # Should expand to cover both functions - assert result["expansion_start"] == 15 - assert result["expansion_end"] == 40 - - -class TestMergeHunks: - """Tests for _merge_hunks function.""" - - def test_returns_single_hunk_unchanged(self): - hunks = [{"expansion_start": 10, "expansion_end": 30}] - merged = _merge_hunks(hunks) - assert merged == hunks - - def test_merges_overlapping_hunks(self): - hunks = [ - { - "expansion_start": 10, - "expansion_end": 30, - "original_hunk": {"header": "@@ -17,7 +17,7 @@"}, - }, - { - "expansion_start": 25, - "expansion_end": 50, - "original_hunk": {"header": "@@ -40,5 +40,5 @@"}, - }, - ] - merged = _merge_hunks(hunks) - - assert len(merged) == 1 - assert merged[0]["expansion_start"] == 10 - assert merged[0]["expansion_end"] == 50 - - def test_merges_adjacent_hunks(self): - hunks = [ - {"expansion_start": 10, "expansion_end": 20}, - {"expansion_start": 21, "expansion_end": 30}, # Adjacent (gap = 1) - ] - merged = _merge_hunks(hunks) - - assert len(merged) == 1 - assert merged[0]["expansion_start"] == 10 - assert merged[0]["expansion_end"] == 30 - - def test_keeps_separate_non_overlapping_hunks(self): - hunks = [ - {"expansion_start": 10, "expansion_end": 20}, - {"expansion_start": 30, "expansion_end": 40}, # Gap of 9 lines - ] - merged = _merge_hunks(hunks) - - assert len(merged) == 2 - - def test_sorts_hunks_by_start(self): - hunks = [ - {"expansion_start": 50, "expansion_end": 60}, - {"expansion_start": 10, "expansion_end": 20}, - ] - merged = _merge_hunks(hunks) - - assert merged[0]["expansion_start"] == 10 - assert merged[1]["expansion_start"] == 50 - - -class TestRebuildPatch: - """Tests for _rebuild_patch function.""" - - def test_rebuilds_single_expanded_hunk(self): - raw_patch = ( - "@@ -17,7 +17,7 @@ def generate_token(user_id: str, ttl_hours: int = 24) -> str:\n" - " expiry = datetime.utcnow() + timedelta(hours=ttl_hours)\n" - ' payload = f"{user_id}:{expiry.isoformat()}"\n' - ' signature = hashlib.sha256(f"{payload}{SECRET_KEY}".encode()).hexdigest()\n' - '- return f"{payload}:{signature}"\n' - '+ return f"{payload}.{signature}"\n' - "\n" - "\n" - " def verify_token(token: str) -> bool:" - ) - - source_lines = [f"line {i}" for i in range(1, 50)] - merged_hunks = [ - { - "original_hunk": { - "header": "@@ -17,7 +17,7 @@", - "lines": [ - " expiry = datetime.utcnow() + timedelta(hours=ttl_hours)", - ' payload = f"{user_id}:{expiry.isoformat()}"', - ' signature = hashlib.sha256(f"{payload}{SECRET_KEY}".encode())' - ".hexdigest()", - '- return f"{payload}:{signature}"', - '+ return f"{payload}.{signature}"', - "", - "", - " def verify_token(token: str) -> bool:", - ], - }, - "expansion_start": 10, - "expansion_end": 30, - "hunk_start_new": 17, - "hunk_end_new": 24, - } - ] - - result = _rebuild_patch(raw_patch, merged_hunks, source_lines) - - assert result is not None - assert "@@ -10," in result # New header starts at expansion_start - - def test_keeps_header_lines(self): - raw_patch = """diff --git a/src/main.py b/src/main.py -index 123..456 789 ---- a/src/main.py -+++ b/src/main.py -@@ -1,3 +1,3 @@ --old -+new""" - - source_lines = ["line1", "line2", "line3"] - merged_hunks = [ - { - "original_hunk": { - "header": "@@ -1,3 +1,3 @@", - "lines": ["-old", "+new"], - }, - "expansion_start": 1, - "expansion_end": 3, - "hunk_start_new": 1, - "hunk_end_new": 2, - } - ] - - result = _rebuild_patch(raw_patch, merged_hunks, source_lines) - - assert "diff --git" in result - assert "index 123..456" in result - - def test_returns_none_for_empty_hunks(self): - result = _rebuild_patch("patch", [], []) - assert result is None - - -class TestCompactPatch: - """Tests for compact_patch function.""" - - def test_returns_original_for_empty_patch(self): - result = compact_patch("", Path("test.py"), Path("/repo")) - assert result == "" - - def test_returns_original_when_no_functions_found(self, tmp_path: Path): - # Create a Python file without functions - source_file = tmp_path / "test.py" - source_file.write_text("x = 1\ny = 2\n") - - patch = """@@ -1,2 +1,2 @@ --x = 1 -+x = 2 - y = 2""" - - mock_parser = MagicMock() - mock_parser.available = True - mock_parser.find_function_definitions.return_value = [] - - result = compact_patch(patch, Path("test.py"), tmp_path, mock_parser) - assert result == patch - - def test_returns_original_when_treesitter_unavailable(self, tmp_path: Path): - patch = """@@ -1,3 +1,3 @@ --old -+new""" - - mock_parser = MagicMock() - mock_parser.available = False - - result = compact_patch(patch, Path("test.py"), tmp_path, mock_parser) - assert result == patch - - def test_returns_original_for_non_code_file(self, tmp_path: Path): - patch = """@@ -1,3 +1,3 @@ --old -+new""" - - result = compact_patch(patch, Path("README.md"), tmp_path) - assert result == patch - - -class TestCompactPatches: - """Tests for compact_patches function.""" - - def test_processes_multiple_scopes(self, tmp_path: Path, monkeypatch): - # Create source files - (tmp_path / "src").mkdir() - (tmp_path / "src" / "main.py").write_text("x = 1\n") - (tmp_path / "src" / "utils.py").write_text("y = 2\n") - - scope1 = ScopeResult( - repo="owner/repo", - subroot="pkg1", - scope_type=ScopeType.LIBRARY, - reason="test", - changed_files=[ - ChangedFile( - filename="src/main.py", - status=FileStatus.MODIFIED, - patch="@@ -1,3 +1,3 @@\n-old\n+new", - ) - ], - ) - scope2 = ScopeResult( - repo="owner/repo", - subroot="pkg2", - scope_type=ScopeType.SERVICE, - reason="test", - changed_files=[ - ChangedFile( - filename="src/utils.py", - status=FileStatus.MODIFIED, - patch="@@ -5,3 +5,3 @@\n-old\n+new", - ) - ], - ) - scopes = [scope1, scope2] - - # Mock TreeSitterParser to return empty functions - call_count = 0 - - def mock_find_functions(self, file_path): - nonlocal call_count - call_count += 1 - return [] - - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", - mock_find_functions, - ) - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None - ) - monkeypatch.setattr("codespy.tools.git.patch_utils.TreeSitterParser.available", True) - - compact_patches(scopes, tmp_path) - - # Should have tried to find functions in both files - assert call_count == 2 - - def test_skips_deleted_files(self, tmp_path: Path, monkeypatch): - scope = ScopeResult( - repo="owner/repo", - subroot=".", - scope_type=ScopeType.LIBRARY, - reason="test", - changed_files=[ - ChangedFile( - filename="src/deleted.py", - status=FileStatus.REMOVED, - patch="@@ -1,3 +0,0 @@\n-line1\n-line2\n-line3", - ) - ], - ) - - call_count = 0 - - def mock_find_functions(self, file_path): - nonlocal call_count - call_count += 1 - return [] - - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", - mock_find_functions, - ) - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None - ) - monkeypatch.setattr("codespy.tools.git.patch_utils.TreeSitterParser.available", True) - - compact_patches([scope], tmp_path) - - # Should not try to find functions in deleted files - assert call_count == 0 - - def test_skips_files_without_patches(self, tmp_path: Path, monkeypatch): - scope = ScopeResult( - repo="owner/repo", - subroot=".", - scope_type=ScopeType.LIBRARY, - reason="test", - changed_files=[ - ChangedFile( - filename="src/empty.py", - status=FileStatus.MODIFIED, - patch=None, - ) - ], - ) - - call_count = 0 - - def mock_find_functions(self, file_path): - nonlocal call_count - call_count += 1 - return [] - - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", - mock_find_functions, - ) - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None - ) - monkeypatch.setattr("codespy.tools.git.patch_utils.TreeSitterParser.available", True) - - compact_patches([scope], tmp_path) - - assert call_count == 0 - - def test_handles_compaction_failure_gracefully(self, tmp_path: Path, monkeypatch): - # Create source file - source_file = tmp_path / "test.py" - source_file.write_text("def func():\n pass\n") - - scope = ScopeResult( - repo="owner/repo", - subroot=".", - scope_type=ScopeType.LIBRARY, - reason="test", - changed_files=[ - ChangedFile( - filename="test.py", - status=FileStatus.MODIFIED, - patch="@@ -1,3 +1,3 @@\n-old\n+new", - ) - ], - ) - - def mock_find_functions(self, file_path): - raise Exception("parse error") - - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.find_function_definitions", - mock_find_functions, - ) - monkeypatch.setattr( - "codespy.tools.git.patch_utils.TreeSitterParser.__init__", lambda self, path: None - ) - monkeypatch.setattr("codespy.tools.git.patch_utils.TreeSitterParser.available", True) - - # Should not raise - compact_patches([scope], tmp_path) - - # Original patch should be preserved - assert scope.changed_files[0].patch == "@@ -1,3 +1,3 @@\n-old\n+new" diff --git a/tests/test_prefrontal_signature.py b/tests/test_prefrontal_signature.py new file mode 100644 index 0000000..2195b58 --- /dev/null +++ b/tests/test_prefrontal_signature.py @@ -0,0 +1,196 @@ +"""Tests for prefrontal_memory signature handling in audit and summary agents.""" + +from unittest.mock import MagicMock, patch + +import dspy +import pytest + +from codespy.agents.review.models import PRContext, ReviewContext + + +class TestSummarizerPrefrontalMemory: + """Tests for Summarizer handling of prefrontal_memory kwarg and signature field.""" + + def test_empty_prefrontal_memory_no_kwarg(self): + """Empty prefrontal_memory: kwarg absent from call.""" + pr_context = PRContext( + repo_slug="github.com/o/r", + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + summary="s", + ) + mock_settings = MagicMock() + mock_settings.is_signature_enabled.return_value = True + mock_settings.memory.enabled = False # Disable memory to simplify + + captured_kwargs = {} + + def capture_ctxsafe(predictor, sig, **kwargs): + # Return a callable that captures its call kwargs + def capture_call(**call_kwargs): + captured_kwargs.update(call_kwargs) + return dspy.Prediction(summary="test summary") + + return capture_call + + with patch("codespy.agents.review.summary.agent.get_settings", return_value=mock_settings): + with patch("codespy.agents.review.summary.agent.get_cost_tracker", return_value=MagicMock()): + with patch("codespy.agents.review.summary.agent.ContextSafe", side_effect=capture_ctxsafe): + from codespy.agents.review.summary.agent import Summarizer + + summarizer = Summarizer() + result = summarizer.forward( + pr_context=pr_context, + changed_file_paths=["a.py"], + patches="diff", + run_id="test-run", + scopes=None, + topics=None, + prefrontal_memory="", + ) + + assert result is not None + # Empty string means no prefrontal_memory kwarg + assert "prefrontal_memory" not in captured_kwargs + + def test_with_prefrontal_memory_kwarg_present(self): + """With prefrontal_memory: kwarg equals text.""" + pr_context = PRContext( + repo_slug="github.com/o/r", + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + summary="s", + ) + mock_settings = MagicMock() + mock_settings.is_signature_enabled.return_value = True + mock_settings.memory.enabled = False # Disable memory to simplify + + captured_kwargs = {} + + def capture_ctxsafe(predictor, sig, **kwargs): + def capture_call(**call_kwargs): + captured_kwargs.update(call_kwargs) + return dspy.Prediction(summary="test summary") + + return capture_call + + with patch("codespy.agents.review.summary.agent.get_settings", return_value=mock_settings): + with patch("codespy.agents.review.summary.agent.get_cost_tracker", return_value=MagicMock()): + with patch("codespy.agents.review.summary.agent.ContextSafe", side_effect=capture_ctxsafe): + from codespy.agents.review.summary.agent import Summarizer + + summarizer = Summarizer() + result = summarizer.forward( + pr_context=pr_context, + changed_file_paths=["a.py"], + patches="diff", + run_id="test-run", + scopes=None, + topics=None, + prefrontal_memory="some recall text", + ) + + assert result is not None + # Verify prefrontal_memory is in call kwargs + assert "prefrontal_memory" in captured_kwargs + assert captured_kwargs["prefrontal_memory"] == "some recall text" + + +class TestAuditorPrefrontalMemory: + """Tests for Auditor handling of prefrontal_memory. + + The critical test: no UnboundLocalError when prefrontal_memory is used + before being assigned (bug fixed by moving assignment before use). + """ + + def test_empty_prefrontal_memory_no_exception(self): + """Empty prefrontal_memory: no exception raised (was UnboundLocalError before fix).""" + pr_context = PRContext( + repo_slug="github.com/o/r", + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + summary="s", + ) + review_context = ReviewContext(pr_context=pr_context, prefrontal_memory="") + mock_settings = MagicMock() + mock_settings.is_signature_enabled.return_value = True + mock_settings.memory.enabled = False # Disable memory to simplify + + # Mock ContextSafe to return a callable that returns a Prediction with the right attrs + def capture_ctxsafe(predictor, sig, **kwargs): + def capture_call(**call_kwargs): + return dspy.Prediction(quality_assessment="ok", recommendation="APPROVE") + + return capture_call + + with patch("codespy.agents.review.audit.agent.get_settings", return_value=mock_settings): + with patch("codespy.agents.review.audit.agent.get_cost_tracker", return_value=MagicMock()): + with patch("codespy.agents.review.audit.agent.ContextSafe", side_effect=capture_ctxsafe): + with patch("codespy.agents.review.audit.agent.SignatureContext"): + with patch("codespy.agents.review.audit.agent.get_cerebral", return_value=None): + with patch("codespy.agents.review.audit.agent.get_episode_store", return_value=None): + from codespy.agents.review.audit.agent import Auditor + + auditor = Auditor() + # This would raise UnboundLocalError before the fix + result = auditor.forward( + review_context=review_context, + all_issues=[], + run_id="test-run", + scopes=None, + topics=None, + ) + + # Should return a tuple (quality_assessment, recommendation) + assert result is not None + assert len(result) == 2 + assert result[0] == "ok" + assert result[1] == "APPROVE" + + def test_with_prefrontal_memory_kwarg_present(self): + """With prefrontal_memory: kwarg passed to auditor call.""" + pr_context = PRContext( + repo_slug="github.com/o/r", + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + summary="s", + ) + review_context = ReviewContext(pr_context=pr_context, prefrontal_memory="some text") + mock_settings = MagicMock() + mock_settings.is_signature_enabled.return_value = True + mock_settings.memory.enabled = False # Disable memory to simplify + + captured_kwargs = {} + + def capture_ctxsafe(predictor, sig, **kwargs): + def capture_call(**call_kwargs): + captured_kwargs.update(call_kwargs) + return dspy.Prediction(quality_assessment="ok", recommendation="APPROVE") + + return capture_call + + with patch("codespy.agents.review.audit.agent.get_settings", return_value=mock_settings): + with patch("codespy.agents.review.audit.agent.get_cost_tracker", return_value=MagicMock()): + with patch("codespy.agents.review.audit.agent.ContextSafe", side_effect=capture_ctxsafe): + with patch("codespy.agents.review.audit.agent.SignatureContext"): + with patch("codespy.agents.review.audit.agent.get_cerebral", return_value=None): + with patch("codespy.agents.review.audit.agent.get_episode_store", return_value=None): + from codespy.agents.review.audit.agent import Auditor + + auditor = Auditor() + result = auditor.forward( + review_context=review_context, + all_issues=[], + run_id="test-run", + scopes=None, + topics=None, + ) + + assert result is not None + # Verify prefrontal_memory is in call kwargs + assert "prefrontal_memory" in captured_kwargs + assert captured_kwargs["prefrontal_memory"] == "some text" diff --git a/tests/test_recall_tool_scope.py b/tests/test_recall_tool_scope.py new file mode 100644 index 0000000..c56a871 --- /dev/null +++ b/tests/test_recall_tool_scope.py @@ -0,0 +1,64 @@ +"""Tests for recall_memory tool scope handling in code_review and supply_chain agents. + +This tests the fix for: `recall_memory` with `reach=local` returned nothing for +code_review and supply_chain because they passed `scope_topic_ids=None` to +get_prefrontal(). The fix ensures they pass `scope_topic_ids=[scope.topic(repo).id]` +so local_tags() returns the project_scope tag. +""" + +import pytest + + +class TestLocalTagsWithScope: + """Unit tests for local_tags returning project_scope tag.""" + + def test_local_tags_returns_project_scope(self): + """local_tags with scope_id returns project_scope tag.""" + # Skip if prefrontal.reach not available (Hindsight not installed) + try: + from codespy.agents.memory.prefrontal.reach import local_tags + except ImportError: + pytest.skip("prefrontal.reach module not available") + + result = local_tags("github.com/o/r", ["scope123"], False) + assert "project_scope:scope123" in result + + def test_local_tags_without_scope_empty(self): + """local_tags with empty scope_ids returns empty list for non-repo scope.""" + try: + from codespy.agents.memory.prefrontal.reach import local_tags + except ImportError: + pytest.skip("prefrontal.reach module not available") + + result = local_tags("github.com/o/r", [], False) + assert result == [] + + +class TestAgentSourceCode: + """Verify agents pass scope_topic_ids to get_prefrontal by inspecting source. + + This checks that the agent code was updated to pass scope_topic_ids. + The actual code flow is: + 1. code_review agent: calls scope.topic(repo_full_name).id + 2. passes scope_topic_ids=[topic_id] to get_prefrontal() + 3. get_prefrontal -> Prefrontal() -> local_tags(scope_topic_ids) + 4. local_tags returns ["project_scope:"] + """ + + def test_code_review_agent_has_scope_topic_id_computation(self): + """Verify code_review/agent.py computes scope_topic_id before get_prefrontal.""" + with open("src/codespy/agents/review/code_review/agent.py") as f: + source = f.read() + # The fix adds: scope_topic_id = scope.topic(repo_full_name).id + assert "scope_topic_id = scope.topic(repo_full_name).id" in source + # And passes: scope_topic_ids=[scope_topic_id] + assert "scope_topic_ids=[scope_topic_id]" in source + + def test_supply_chain_agent_has_scope_topic_id_computation(self): + """Verify supply_chain/agent.py computes scope_topic_id before get_prefrontal.""" + with open("src/codespy/agents/review/supply_chain/agent.py") as f: + source = f.read() + # The fix adds: scope_topic_id = scope.topic(repo_full_name).id + assert "scope_topic_id = scope.topic(repo_full_name).id" in source + # And passes: scope_topic_ids=[scope_topic_id] + assert "scope_topic_ids=[scope_topic_id]" in source diff --git a/tests/test_review_memories.py b/tests/test_review_memories.py new file mode 100644 index 0000000..463014c --- /dev/null +++ b/tests/test_review_memories.py @@ -0,0 +1,203 @@ +"""Tests for the collapsible memories section of the review output.""" + +from codespy.workflows.review.models import ( + TRUNCATED_MARKER, + MemorySection, + RecalledMemory, + ReviewResult, +) +from codespy.workflows.review.reporters.git import GitReporter + + +def _result( + *memories: tuple[str, list[tuple[str, str]]] | tuple[str, list[tuple[str, str]], bool], + recommendation: str | None = "approve", +) -> ReviewResult: + """Build a ReviewResult with memories from (task, [(section_title, section_body), ...]) tuples. + + Optional third element in tuple is nested flag (default True). + """ + recalled = [] + for item in memories: + task = item[0] + sections = item[1] + nested = item[2] if len(item) > 2 else True + recalled.append( + RecalledMemory( + task=task, + sections=[MemorySection(title=t, text=b) for t, b in sections], + nested=nested, + ) + ) + return ReviewResult( + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + repo="o/r", + model_used="m", + recommendation=recommendation, + memories=recalled, + ) + + +def _git_body(result: ReviewResult) -> str: + reporter = GitReporter.__new__(GitReporter) + return reporter._build_review_body(result, []) + + +def test_no_memories_no_section(): + assert "memories" not in _result().to_markdown() + assert "memories" not in _git_body(_result()) + + +def test_single_memory_nested_structure(): + """Single memory still gets nested
: outer > recall > sections.""" + md = _result(("scope", [("Around this work", "fact A"), ("Decisions", "fact B")])).to_markdown() + # Nested: outer (1) + recall (1) + 2 sections (2) = 4
+ # With blockquote: each
has one
+ assert md.count("
") == 4 + assert md.count("
") == 4 + # Each details+blockquote pair should have balanced tags + assert md.count("
") == md.count("
") + # No ### task heading - task is in + assert "### scope" not in md + assert "scope" in md + # Sections are in summaries + assert "Around this work" in md + assert "Decisions" in md + + +def test_flat_memory_no_recall_wrapper(): + """Flat (nested=False) memory has no task wrapper.""" + md = _result(("review", [("Around this work", "fact A"), ("Decisions", "fact B")], False)).to_markdown() + # Flat: outer (1) + 2 sections (2) = 3
(no recall wrapper) + assert md.count("
") == 3 + assert md.count("
") == 3 + # No review since it's flat + assert "review" not in md + # Sections are in summaries + assert "Around this work" in md + assert "Decisions" in md + + +def test_memories_before_summary(): + """Memories section must appear before ## Summary in to_markdown output.""" + md = _result(("scope", [("Around this work", "fact A")])).to_markdown() + assert md.index("memories") < md.index("## Summary") + + +def test_git_body_memories_before_summary(): + """Memories section must appear before 📋 Summary in git body.""" + body = _git_body(_result(("scope", [("Around this work", "fact A")]))) + assert "🧠 memories" in body + assert body.index("🧠 memories") < body.index("📋 Summary") + + +def test_multiple_memories_flat_before_scope(): + """Multiple recalls: flat first, then scope, with correct structure.""" + md = _result( + ("review", [("Decisions", "review decisions")], False), # flat + ("scope", [("Around this work", "scope context")]), # nested + ).to_markdown() + # Flat + nested: outer (1) + 1 section (flat) + 1 recall (scope) + 1 section (nested) = 4
+ assert md.count("
") == 4 + assert md.count("
") == 4 + # Balanced blockquote tags + assert md.count("
") == md.count("
") + # Flat sections appear before scope + assert md.index("Decisions") < md.index("scope") + # Flat sections don't have review + assert "review" not in md + # Outer section closes last + assert md.rstrip().endswith("
") + + +def test_section_with_subheadings_stays_in_one_details(): + """A section body containing '## ' must render as exactly one
block.""" + body_with_subheadings = "## Service Structure\nContent\n## Another subsection\nMore content" + md = _result( + ("scope", [("Around this work", body_with_subheadings), ("Decisions", "fact")]) + ).to_markdown() + # nested: outer (1) + recall (1) + 2 sections (2) = 4
+ assert md.count("
") == 4 + assert md.count("
") == 4 + # Balanced blockquote tags + assert md.count("
") == md.count("
") + + +def test_empty_sections_not_rendered(): + """Empty memories list produces no memories section.""" + result = ReviewResult( + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + repo="o/r", + model_used="m", + recommendation="approve", + memories=[RecalledMemory(task="scope", sections=[])], + ) + assert result.memories_markdown_lines() == [] + + +def test_git_body_truncates_to_budget(): + """Large memories are truncated to fit within MAX_BODY_CHARS.""" + big = "x" * 50_000 + body = _git_body(_result(("scope", [("Around this work", big)]), ("review", [("Decisions", big)], False))) + assert len(body) <= GitReporter.MAX_BODY_CHARS + assert TRUNCATED_MARKER in body + assert body.count("
") == body.count("
") + assert body.count("
") == body.count("
") + + +def test_budget_too_small_drops_section(): + """When budget is too small, memories section is dropped entirely.""" + result = _result(("scope", [("Around this work", "abc")])) + assert result.memories_markdown_lines(max_chars=10) == [] + + +def test_balanced_tags_when_truncated(): + """When truncating, all
and
tags remain balanced.""" + big = "x" * 100_000 + result = _result(("review", [("Around this work", big), ("Decisions", big)], False)) + lines = result.memories_markdown_lines(max_chars=5000) + text = "\n".join(lines) + assert text.count("
") == text.count("
") + assert text.count("
") == text.count("
") + assert TRUNCATED_MARKER in text + + +def test_flat_structure_tag_counts(): + """Flat single recall with 2 sections -> 3 details + 3 blockquotes.""" + md = _result(("review", [("Briefings", "b1"), ("Around this work", "b2")], False)).to_markdown() + # Flat: outer + 2 sections = 3
+ assert md.count("
") == 3 + assert md.count("
") == 3 + # Each details has one blockquote wrapper + assert md.count("
") == 3 + assert md.count("
") == 3 + + +def test_flat_plus_scope_structure_tag_counts(): + """Flat + scope (1 section each) -> 4 details + 4 blockquotes.""" + md = _result( + ("review", [("Briefings", "b1")], False), + ("scope", [("Around this work", "s1")]), + ).to_markdown() + # Flat + nested: outer + 1 flat section + scope recall + 1 nested section = 4
+ assert md.count("
") == 4 + assert md.count("
") == 4 + # Each details has one blockquote wrapper + assert md.count("
") == 4 + assert md.count("
") == 4 + + +def test_order_flat_before_scope(): + """Flat sections must appear before scope.""" + md = _result( + ("review", [("Briefings", "b1"), ("Decisions", "d1")], False), + ("scope", [("Around this work", "s1")]), + ).to_markdown() + # Find positions of flat section and scope + briefings_pos = md.index("Briefings") + scope_pos = md.index("scope") + assert briefings_pos < scope_pos diff --git a/tests/test_review_statistics.py b/tests/test_review_statistics.py new file mode 100644 index 0000000..886dc6e --- /dev/null +++ b/tests/test_review_statistics.py @@ -0,0 +1,123 @@ +"""Tests for the severity × category statistics matrix in review output.""" + +from codespy.agents.review.models import Issue, IssueCategory, IssueSeverity +from codespy.workflows.review.models import ReviewResult +from codespy.workflows.review.reporters.git import GitReporter + + +def _make_issue(severity: IssueSeverity, category: IssueCategory) -> Issue: + """Create a minimal Issue with given severity and category.""" + return Issue( + category=category, + severity=severity, + title="test issue", + description="test description", + filename="test.py", + ) + + +def _result_with_issues(*issues: Issue) -> ReviewResult: + """Build a ReviewResult with the given issues.""" + return ReviewResult( + pr_number=1, + pr_title="t", + pr_url="https://github.com/o/r/pull/1", + repo="o/r", + model_used="m", + issues=list(issues), + ) + + +def _git_body(result: ReviewResult) -> str: + """Build the git review body from a result.""" + reporter = GitReporter.__new__(GitReporter) + return reporter._build_review_body(result, []) + + +def test_statistics_matrix_header(): + """The statistics table should have the correct header row.""" + result = _result_with_issues() + body = _git_body(result) + + assert "| Severity | Security | Bugs | Documentation | Smells | Total |" in body + assert "|----------|----------|------|---------------|--------|-------|" in body + + +def test_statistics_matrix_all_severities(): + """The statistics table should include all severity rows.""" + result = _result_with_issues() + body = _git_body(result) + + assert "| Critical |" in body + assert "| High |" in body + assert "| Medium |" in body + assert "| Low |" in body + assert "| Info |" in body + + +def test_statistics_matrix_zero_issues(): + """When there are no issues, all cells should show 0.""" + result = _result_with_issues() + body = _git_body(result) + + # Check that we have zeros in the matrix (each severity row has 6 zeros) + # The pattern is: | Severity | 0 | 0 | 0 | 0 | 0 | + lines = body.split("\n") + data_rows = [l for l in lines if l.startswith("| Critical |") or l.startswith("| High |") or + l.startswith("| Medium |") or l.startswith("| Low |") or l.startswith("| Info |")] + + for row in data_rows: + assert "| 0 | 0 | 0 | 0 | 0 |" in row, f"Expected zeros in row: {row}" + + # Check totals row + assert "| **Total** | **0** | **0** | **0** | **0** | **0** |" in body + + +def test_statistics_matrix_counts_issues(): + """Issues should be counted in the correct severity × category cell.""" + result = _result_with_issues( + _make_issue(IssueSeverity.CRITICAL, IssueCategory.SECURITY), + _make_issue(IssueSeverity.HIGH, IssueCategory.SECURITY), + _make_issue(IssueSeverity.HIGH, IssueCategory.BUG), + _make_issue(IssueSeverity.MEDIUM, IssueCategory.DOCUMENTATION), + _make_issue(IssueSeverity.INFO, IssueCategory.SMELL), + ) + body = _git_body(result) + + lines = body.split("\n") + data_rows = {l.split("|")[1].strip(): l for l in lines if l.startswith("| ") and "Severity" not in l} + + # Critical row: 1 security, 0 others + assert "| Critical | 1 | 0 | 0 | 0 | 1 |" in body + + # High row: 1 security, 1 bug, 0 others + assert "| High | 1 | 1 | 0 | 0 | 2 |" in body + + # Medium row: 1 documentation + assert "| Medium | 0 | 0 | 1 | 0 | 1 |" in body + + # Info row: 1 smell + assert "| Info | 0 | 0 | 0 | 1 | 1 |" in body + + # Totals row + assert "| **Total** | **2** | **1** | **1** | **1** | **5** |" in body + + +def test_statistics_matrix_no_old_metric_table(): + """The old Metric | Count table format should not appear.""" + result = _result_with_issues() + body = _git_body(result) + + assert "| Metric | Count |" not in body + assert "| Total Issues |" not in body + + +def test_statistics_matrix_in_collapsible(): + """The statistics section should be inside a collapsible details block.""" + result = _result_with_issues() + body = _git_body(result) + + # Check for the collapsible wrapper + assert '
' in body + assert '📊 Statistics' in body + assert '
' in body diff --git a/tests/test_scope_resolver.py b/tests/test_scope_resolver.py index 8c029a6..80543aa 100644 --- a/tests/test_scope_resolver.py +++ b/tests/test_scope_resolver.py @@ -20,7 +20,8 @@ def test_single_scope_indicator(self): """Test deriving sparse paths with single scope indicator.""" changed_files = ["packages/auth/src/index.ts"] paths = derive_sparse_paths(changed_files) - assert "packages/auth/" in paths + # Anchored paths start with / + assert "/packages/auth/" in paths assert "/*" in paths def test_multiple_scope_indicators(self): @@ -30,15 +31,17 @@ def test_multiple_scope_indicators(self): "packages/auth/src/index.ts", ] paths = derive_sparse_paths(changed_files) - assert "mono/svc/api/" in paths - assert "packages/auth/" in paths + # Anchored paths start with / + assert "/mono/svc/api/" in paths + assert "/packages/auth/" in paths assert "/*" in paths def test_no_scope_indicator_uses_depth_fallback(self): """Test fallback to depth-2 when no indicator found.""" changed_files = ["backend/api/handler.go"] paths = derive_sparse_paths(changed_files) - assert "backend/api/" in paths + # Anchored paths start with / + assert "/backend/api/" in paths def test_root_level_file(self): """Test root-level files don't add extra paths.""" @@ -55,8 +58,8 @@ def test_deduplication(self): "packages/auth/tests/auth.test.ts", ] paths = derive_sparse_paths(changed_files) - # Should only have one packages/auth/ - assert paths.count("packages/auth/") == 1 + # Should only have one /packages/auth/ (anchored) + assert paths.count("/packages/auth/") == 1 class TestScopeResolver: @@ -204,8 +207,30 @@ def test_internal_dir_not_scope_indicator(self): """internal/ is not a scope indicator for sparse paths.""" changed_files = ["backend/internal/cache/redis.go"] paths = derive_sparse_paths(changed_files) - # Should use depth-2 fallback, not scope indicator - assert "backend/internal/" in paths # depth-2 prefix + # Should use depth-2 fallback, not scope indicator (anchored) + assert "/backend/internal/" in paths # depth-2 prefix + + def test_anchored_manifest_patterns(self): + """Manifest patterns are anchored to ancestor dirs, not repo-wide.""" + changed_files = ["packages/auth/src/index.ts"] + paths = derive_sparse_paths(changed_files) + # Should have anchored manifest patterns, not bare manifest names + assert "/go.mod" in paths # root manifest + assert "/packages/go.mod" in paths # ancestor manifest + assert "/packages/auth/go.mod" in paths # scope manifest + # Should NOT have bare repo-wide patterns + assert "go.mod" not in paths or paths.count("go.mod") == 0 + + def test_anchored_ai_file_patterns(self): + """AI instruction files are anchored to ancestor dirs.""" + changed_files = ["packages/auth/src/index.ts"] + paths = derive_sparse_paths(changed_files) + # Should have anchored AI patterns + assert "/.kilo/" in paths # root AI dir + assert "/packages/.kilo/" in paths # ancestor AI dir + assert "/packages/auth/.kilo/" in paths # scope AI dir + assert "/AGENTS.md" in paths # root AI file + assert "/packages/AGENTS.md" in paths # ancestor AI file def test_indicator_suppressed_under_manifest(self): """Indicator scopes not created when parent manifest covers file."""