From 92d680523e25ac89585b7a7b7a1b8497488b9162 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:44:19 +0300 Subject: [PATCH 01/63] feat(mcp): scaffold api/mcp module with FastMCP server and cgraph-mcp entry point Add the bare MCP server module (api/mcp/) using the official FastMCP SDK, wire the cgraph-mcp console script in pyproject.toml, and include a protocol smoke test that spawns the server over stdio and verifies list_tools returns an empty tool set. Also copies the MCP design docs into docs/. Closes #648 Co-Authored-By: Claude Opus 4.6 (1M context) --- api/mcp/__init__.py | 8 + api/mcp/server.py | 26 +++ docs/MCP_SERVER_DESIGN.md | 321 ++++++++++++++++++++++++++++++++++++ docs/code-graph-mcp-v4.docx | Bin 0 -> 24117 bytes pyproject.toml | 2 + tests/mcp/__init__.py | 0 tests/mcp/test_scaffold.py | 55 ++++++ uv.lock | 138 ++++++++++++++++ 8 files changed, 550 insertions(+) create mode 100644 api/mcp/__init__.py create mode 100644 api/mcp/server.py create mode 100644 docs/MCP_SERVER_DESIGN.md create mode 100644 docs/code-graph-mcp-v4.docx create mode 100644 tests/mcp/__init__.py create mode 100644 tests/mcp/test_scaffold.py diff --git a/api/mcp/__init__.py b/api/mcp/__init__.py new file mode 100644 index 00000000..c161c8e1 --- /dev/null +++ b/api/mcp/__init__.py @@ -0,0 +1,8 @@ +"""MCP server module for code-graph. + +Exposes the code-graph indexer and graph queries as MCP tools so AI coding +agents (Claude Code, Cursor, Copilot, Roo/Cline) can drive the indexer over +the standard Model Context Protocol stdio transport. + +Entry point: ``cgraph-mcp`` (defined in ``pyproject.toml``). +""" diff --git a/api/mcp/server.py b/api/mcp/server.py new file mode 100644 index 00000000..d81111df --- /dev/null +++ b/api/mcp/server.py @@ -0,0 +1,26 @@ +"""FastMCP server for code-graph. + +This is the scaffold (T1). It instantiates a single FastMCP app, exposes it +as ``app`` for tests and embedders, and registers a ``main()`` entry point +that runs the server over stdio. Tools are registered in later tickets +(T4-T8, T11) by importing this module's ``app`` and decorating functions +with ``@app.tool(...)``. +""" + +from __future__ import annotations + +from mcp.server.fastmcp import FastMCP + +app: FastMCP = FastMCP("code-graph") + + +def main() -> None: + """Run the MCP server over stdio. + + Console-script entry point for ``cgraph-mcp``. + """ + app.run() + + +if __name__ == "__main__": + main() diff --git a/docs/MCP_SERVER_DESIGN.md b/docs/MCP_SERVER_DESIGN.md new file mode 100644 index 00000000..f1b9c1eb --- /dev/null +++ b/docs/MCP_SERVER_DESIGN.md @@ -0,0 +1,321 @@ +# Code Graph MCP Server — Design Summary + +## What We're Building + +An MCP server inside the `code-graph` repo that gives AI coding agents two capabilities: +1. **Structural tools** (deterministic, no LLM): get_callers, get_callees, get_dependencies, impact_analysis, find_path, search_code, index_repo +2. **Ask tool** (GraphRAG SDK, needs LLM): natural language questions about the codebase → NL-to-Cypher → grounded answers + +Phase 1 also bundles three foundational improvements to `api/` that the MCP server needs and that benefit all consumers of code-graph (CLI, web UI, future integrations): + +3. **Multi-branch graph identity**: per `(repo, branch)` graph naming so concurrent agents on different branches can't corrupt each other's view. (Today, two users on different branches overwrite each other's graphs — this is a real bug.) +4. **Incremental indexing**: file-hash-based skip-unchanged so agents can call `index_repo` cheaply on every interaction. Default-on once a graph exists. +5. **Tree-sitter language expansion**: a shared `TreeSitterAnalyzer` base class plus 5 new languages (Go, Rust, TypeScript, Ruby, C++) and re-enabling C. Brings supported languages from 5 to 11. + +## Key Decisions Made + +- **Mono-repo**: Build inside `code-graph/api/mcp/`, not a separate project. One pip package, one repo. +- **Module path is `api/mcp/`, NOT top-level `mcp/`**: A top-level `mcp/` directory would shadow the installed `mcp` PyPI SDK and break `from mcp.server.fastmcp import FastMCP`. Entry point: `cgraph-mcp = "api.mcp.server:app"`. +- **Python MCP server**: Use the official `mcp` Python SDK (`from mcp.server.fastmcp import FastMCP`), NOT standalone `fastmcp` or Node.js. Avoids language bridge. +- **Reuse everything**: Most code already exists. The MCP tools are thin wrappers around `api/graph.py`, `api/project.py`, `api/cli.py`, and `api/llm.py`. +- **GraphRAG SDK powers the ask tool**: `kg.ask()` does NL-to-Cypher. Code-graph's `api/llm.py` already integrates this — repackage for MCP. +- **Reuse the existing hand-coded ontology** from `api/llm.py:_define_ontology()` (lines 26–233) rather than auto-extracting via `Ontology.from_kg_graph()`. The hand-coded version has richer entity attributes and descriptions tuned for code. Refactor: rename `_define_ontology` → `define_ontology` so the MCP module can import it. +- **Ship with 3 languages** (Python/Java/C#), add tree-sitter for broad coverage in Phase 2. +- **No incremental indexing in v1**: Full re-indexing is sufficient. Deferred to Phase 3. +- **No Graphiti/memory or raw FalkorDB MCP in v1**: Out of scope. Available as separate servers. Architecture supports merging later. +- **Auto-init for zero config**: ensure-db auto-starts FalkorDB Docker, auto-index on first tool call, auto-GraphRAG init. +- **Expose Cypher in ask responses**: Transparency for the agent + learning patterns. +- **Stdio transport only in Phase 1**: HTTP/SSE deferred to Phase 1.5. Stdio is sufficient for Claude Code, Cursor, and Claude Desktop. +- **`impact_analysis` defaults**: direction `IN` (upstream callers — "what breaks if I change this"), depth `3`, max depth clamp `10`. Parameters allow override. +- **Multi-branch graph identity**: graph name is `code:{project}:{branch}`. Sourcegraph-zoekt-style isolation. The IDE/LSP "single current index, re-index on switch" model only works because IDEs are single-tenant; code-graph is multi-tenant (a server, not a desktop tool, with concurrent agents on different branches), so per-branch isolation is required for correctness. Existing single-graph deployments migrate to `code:{project}:_default`. Within a branch, the existing transition-based `switch_commit` flow continues to work unchanged. +- **`index_repo` auto-detects branch**: from `git rev-parse --abbrev-ref HEAD` in the target path. Optional override parameter. Non-git paths use `_default`. +- **Incremental indexing default-on**: once a `(project, branch)` graph exists, `index_repo` and `cgraph index` auto-detect and run incremental. Opt out with `--full` (CLI) or `incremental=False` (MCP). First-time runs are full. Tracks per-file SHA256 hashes in Redis under `{repo}:{branch}_files`. Built on the existing `delete_files()` primitive in `api/graph.py` (today only used in the git-history flow). +- **Tree-sitter base class**: refactor existing PythonAnalyzer / JavaScriptAnalyzer / KotlinAnalyzer onto a shared `TreeSitterAnalyzer` base in Phase 1. Five new languages added on the new base in the same phase. C is re-enabled. Java and C# stay on multilspy (LSP) until a future phase. + +## Directory Structure + +``` +code-graph/ +├── api/ # Existing Python backend (FastAPI, analyzers, graph, llm, cli) +│ └── mcp/ # NEW — MCP server module (under api/ to avoid shadowing the installed `mcp` SDK) +│ ├── __init__.py +│ ├── server.py # FastMCP entry point, tool registration, stdio transport +│ ├── auto_init.py # ensure-db + auto-index hooks +│ ├── graphrag_init.py # KnowledgeGraph construction + per-project caching (reuses api/llm.py:define_ontology) +│ ├── code_prompts.py # Re-exports + hooks for GraphRAG prompts (sourced from api/prompts.py) +│ ├── templates/ # Agent guidance file templates (cursorrules, claude_mcp_section) +│ └── tools/ +│ ├── __init__.py +│ ├── structural.py # index_repo, get_callers, get_callees, get_dependencies, +│ │ # impact_analysis, find_path, search_code +│ └── ask.py # GraphRAG-powered ask tool +├── app/ # Existing React frontend +├── skills/ # Existing Claude Code skill +├── tests/ +│ └── mcp/ +│ ├── __init__.py +│ ├── conftest.py # Session-scoped FalkorDB + indexed-fixture fixtures +│ ├── fixtures/ +│ │ ├── sample_project/ # Py/Java/C# fixture with known call graph +│ │ └── expected.yaml # Assertion contract: counts, callers, callees, paths, search hits +│ ├── test_scaffold.py # Scaffold smoke test (T1) +│ ├── test_index_repo.py # T4 — unit + integration + protocol +│ ├── test_neighbors.py # T5 — unit + integration + protocol + CLI parity +│ ├── test_impact_analysis.py # T6 +│ ├── test_find_path.py # T7 +│ ├── test_search_code.py # T8 +│ ├── test_graphrag_init.py # T9 +│ ├── test_code_prompts.py # T10 (snapshot) +│ ├── test_ask.py # T11 — mocked LLM, real Cypher against fixture +│ ├── test_auto_init.py # T12 +│ └── test_init_agent.py # T13 +└── pyproject.toml # Adds `cgraph-mcp = "api.mcp.server:app"` and `mcp>=1.0,<2.0` +``` + +**Note on test layout:** Each tool ticket ships its own integration + MCP-protocol round-trip tests in the same PR — there is no separate "integration tests" or "protocol tests" milestone. The previous `integration/` and `e2e/` subdirectories are removed in favor of per-tool test files. Real-LLM E2E is deferred to Phase 1.5. + +## CLI-to-MCP Tool Mapping + +| cgraph Command | MCP Tool | Shared Code | Delta | +|---|---|---|---| +| `cgraph index` / `index-repo` | `index_repo` | api/project.py, analyzers/ | + GraphRAG init after indexing | +| `cgraph neighbors --rel CALLS --dir in` | `get_callers` | api/graph.py Cypher | Thin wrapper | +| `cgraph neighbors --rel CALLS --dir out` | `get_callees` | api/graph.py Cypher | Thin wrapper | +| `cgraph neighbors` (multi-rel) | `get_dependencies` | api/graph.py Cypher | New multi-rel query | +| (new) | `impact_analysis` | api/graph.py Cypher | New variable-depth traversal | +| `cgraph paths` | `find_path` | api/graph.py Cypher | Thin wrapper | +| `cgraph search` | `search_code` | api/auto_complete.py | Thin wrapper | +| (web UI chat) | `ask` | api/llm.py + GraphRAG SDK | Repackage as MCP tool | + +## GraphRAG SDK Integration Pattern + +The MCP `ask` tool is a thin wrapper around the GraphRAG SDK flow already implemented in `api/llm.py`. End-to-end: + +1. **Pre-step (once per project, in `api/mcp/graphrag_init.py`):** construct a `KnowledgeGraph` and cache it. Reuses the existing hand-coded ontology from `api/llm.py:define_ontology()` (renamed from `_define_ontology` in T9). + +2. **Per-call (in the `ask` tool):** retrieve cached `KnowledgeGraph`, call `kg.ask(question)`, return `{answer, cypher_query, context_nodes}`. Internally this is **two LLM round-trips bracketing one Cypher query against FalkorDB**: + - LLM #1: question + ontology → Cypher + - FalkorDB: execute Cypher → rows + - LLM #2: question + rows → natural-language answer + + The graph itself never goes to the LLM — only schema and query results — which is why this works on huge codebases. + +```python +# api/mcp/graphrag_init.py — construct once, cache per project +from graphrag_sdk import KnowledgeGraph +from graphrag_sdk.models.litellm import LiteModel +from graphrag_sdk.model_config import KnowledgeGraphModelConfig + +from api.llm import define_ontology # renamed from _define_ontology in T9 +from api.mcp.code_prompts import ( + CYPHER_GEN_SYSTEM, CYPHER_GEN_PROMPT, + GRAPH_QA_SYSTEM, GRAPH_QA_PROMPT, +) + +_kg_cache: dict[str, KnowledgeGraph] = {} + +def get_or_create_kg(project_name: str) -> KnowledgeGraph: + if project_name in _kg_cache: + return _kg_cache[project_name] + + model = LiteModel(model_name=os.getenv("MODEL_NAME", "gemini/gemini-flash-lite-latest")) + kg = KnowledgeGraph( + name=f"code:{project_name}", + model_config=KnowledgeGraphModelConfig.with_model(model), + ontology=define_ontology(), # REUSE — do NOT call Ontology.from_kg_graph + cypher_system_instruction=CYPHER_GEN_SYSTEM, + qa_system_instruction=GRAPH_QA_SYSTEM, + cypher_gen_prompt=CYPHER_GEN_PROMPT, + qa_prompt=GRAPH_QA_PROMPT, + ) + _kg_cache[project_name] = kg + return kg + +# api/mcp/tools/ask.py — the tool itself +async def ask(question: str) -> dict: + kg = get_or_create_kg(current_project_name()) + response = await asyncio.get_event_loop().run_in_executor(None, kg.ask, question) + return { + "answer": response.answer, + "cypher_query": response.cypher_query, # exposed for transparency + "context_nodes": response.context_nodes, + } +``` + +## Multi-Branch Graph Identity + +**Problem.** Today, `Graph(project_name)` (api/project.py:225, api/index.py:246) names every graph after the repo directory. There is no branch component. Two users (or agents) indexing the same repo on different branches **silently overwrite each other** — the second indexer wipes the first. The Redis metadata under `{repo}_info` (api/info.py:33-46) is also a single global hash per repo, so the commit pointer is shared. This is a real bug that the MCP server will hit immediately because agents working on PR branches need their branch's view, not stale state from someone else's main. + +**Comparison with prior art.** + +| System | Storage | Branch behavior | Why it works for them | +|---|---|---|---| +| JetBrains IDEs | Single on-disk index | Filesystem-state-based; re-index on `git checkout` | Single-tenant; one developer, one workspace | +| VS Code + LSPs | In-memory per workspace + content-hash cache | LSP receives `didChangeWatchedFiles`, reanalyzes touched files | Single-tenant; each workspace is isolated by process | +| GitHub Blackbird | Server-side, commit-sharded, deduped | All commits indexed simultaneously | Massive scale; full historical search | +| Sourcegraph zoekt | Per `(repo, branch)` shard | Default branch always indexed; others by config | Server, multi-tenant, interactive use | +| Sourcegraph SCIP | Per-commit graph data uploaded by CI | Commit-keyed | Reproducible cross-commit code intel | + +Code-graph is architecturally a server (FalkorDB-backed, accessed by multiple clients), so the IDE single-index model is wrong for it. The closest analog is **Sourcegraph zoekt: per `(repo, branch)` graphs**. + +**Solution (T17).** Graph naming becomes `code:{project_name}:{branch}`. The full set of changes: + +| File | Change | +|---|---| +| `api/graph.py` | `Graph` and `AsyncGraphQuery` constructors take a `branch` parameter; default `_default`; graph name composed as `code:{project}:{branch}` | +| `api/project.py` | `Project.from_git_repository()` and `Project.from_local_directory()` accept and propagate `branch`; auto-detect via `git rev-parse --abbrev-ref HEAD` when `None` | +| `api/info.py` | Redis metadata keys become `{repo}:{branch}_info`; `set_repo_commit`, `get_repo_commit`, etc. take a branch param | +| `api/git_utils/` | Git-transitions graph becomes `{repo}:{branch}_git`; `switch_commit` stays scoped to a single branch graph | +| `api/cli.py` | `cgraph index --branch `; `cgraph list` enumerates `(project, branch)` pairs; `cgraph info`, `cgraph search` accept `--branch` | +| `api/index.py` | `/api/list_repos`, `/api/graph_entities`, `/api/repo_info`, `/api/get_neighbors`, `/api/find_paths`, `/api/auto_complete` accept optional `branch` query param; responses include the branch | +| MCP `index_repo` | Accepts optional `branch`; auto-detects from target path's checkout; returns `branch` in response | + +**Migration.** A one-shot helper renames `code:{project}` → `code:{project}:_default` and copies `{repo}_info` → `{repo}:_default_info` on first read. Documented as `cgraph migrate` for explicit invocation. + +**Out of scope for Phase 1.** Cross-branch query tools, branch comparison, branch garbage collection. Each branch is an isolated graph; users prune via `cgraph delete --branch`. + +## Incremental Indexing + +**Today.** `SourceAnalyzer.first_pass()` (`api/analyzers/source_analyzer.py:82-121`) re-parses **every supported file** on every index call. There is no per-file hash tracking. The codebase already has the primitives for incremental work — `Graph.delete_files()` (referenced in `api/git_utils/git_utils.py:153, 217`) and the file-classification logic in `classify_changes` — but they're coupled to the git-history-build flow and not used for ad-hoc reindexing. + +**Solution (T18).** Add file-hash-based incremental indexing on top of T17's per-branch storage. Flow: + +1. **Hash store**: Redis hash `{repo}:{branch}_files` mapping `file_path → SHA256(content)`. Persisted at the end of every full or incremental index. +2. **Diff phase**: `Project.analyze_sources(incremental=True)` walks the file tree, computes current hashes, diffs against the stored map: + - **Unchanged** → skip the analyzer entirely + - **Modified** → call `delete_files([path])` to remove old graph entities, then re-run the analyzer (first pass) on the file + - **Deleted** → call `delete_files([path])` only + - **New** → analyze normally +3. **Second-pass (LSP) decision**: if any file changed, run the LSP-based second pass over the entire branch graph. Per-file second-pass is a Phase 2 optimization (correctness over speed in v1). +4. **Persist** the new hash map. + +**Defaults.** +- First run on a fresh `(project, branch)` → automatically falls back to full +- Hash store missing or corrupted → falls back to full with a warning logged +- File renames → treated as delete + add (rename detection deferred to Phase 2) +- CLI `cgraph index .` defaults to incremental when a graph exists; `--full` forces full +- MCP `index_repo` defaults to incremental; response includes `mode: "full"|"incremental"` and `files_changed: list[str]` + +**Why this is the right primitive for agents.** Agents call `index_repo` reflexively at the start of every interaction to ensure freshness. With full re-index, this is too slow to be reflexive. With incremental + content-hash skipping (the same trick LSP servers use), the steady-state cost is "diff a few file hashes" — acceptable for every-call use. + +## Competitive Context + +5 competitors exist: codebase-memory-mcp (66 langs, SQLite), GitNexus (23.8K stars, KuzuDB), Codegraph (11 langs, 30+ tools, SQLite), CodeGraphContext (Neo4j), Code Pathfinder (Python only). + +**All share 3 gaps FalkorDB fills:** +1. No NL query layer (none have an "ask" tool — GraphRAG SDK is the differentiator) +2. Local-only embedded storage (all SQLite — FalkorDB is client-server, supports shared/team/cloud) +3. No ecosystem path to memory/intelligence (FalkorDB has Graphiti, GraphRAG SDK, mcpserver) + +**Don't compete on:** tool count or language count. +**Compete on:** ask tool (understanding), shared graphs (scale), cloud path (enterprise). + +## CI Testing Strategy + +**Each tool ticket ships its own four kinds of tests in the same PR.** No bulk-testing milestones at the end. + +| Layer | Runs On | FalkorDB | LLM | What It Tests | +|---|---|---|---|---| +| Unit tests | Every PR | Mocked | No | Parameter parsing, Cypher generation, output formatting, error handling | +| Integration (structural) | Every PR | Docker service | No | Tool against indexed fixture, asserted via `expected.yaml` contract | +| Integration (ask, mocked) | Every PR | Docker service | Mocked | GraphRAG init, prompt construction, real Cypher execution against fixture, answer formatting | +| MCP protocol round-trip | Every PR | Docker service | No | `session.list_tools()` schema check + `session.call_tool(...)` round-trip via the `mcp` SDK's stdio client | +| CLI parity (where applicable) | Every PR | Docker service | No | MCP tool output matches the equivalent `cgraph` CLI command output | +| E2E (ask, real LLM) | **Phase 1.5** (nightly, deferred) | Docker service | Real (secret) | Prompt quality, answer grounding, regression detection | + +GitHub Actions FalkorDB service (added in T2): +```yaml +services: + falkordb: + image: falkordb/falkordb:latest + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 +``` + +## GitHub Issues Breakdown (Phase 1: 18 vertical issues) + +Each tool ticket ships impl + unit + integration + protocol round-trip in a single PR. There are no separate "testing" milestones — testing is folded into every ticket. + +### Foundation +1. **T1 — Scaffold `api/mcp/` module + `cgraph-mcp` entry point.** FastMCP server, stdio runner, `mcp>=1.0,<2.0` dep, copy design doc into repo, scaffold smoke test. +2. **T2 — CI workflow with FalkorDB service.** `.github/workflows/mcp-tests.yml`, FalkorDB service container, runs on path filter, scaffold smoke test green. +3. **T3 — Test fixture project + assertion contract.** `tests/mcp/fixtures/sample_project/` with known call graph in Py/Java/C#, plus `expected.yaml` and session-scoped `conftest.py`. + +### Core `api/` improvements (prerequisite to MCP tools) +17. **T17 — Per-branch graph identity.** `code:{project}:{branch}` naming everywhere; branch param on `Graph`, `Project`, `info`, CLI, REST endpoints; one-shot migration helper to `_default`. **On the critical path before T4.** +18. **T18 — Incremental indexing.** File-hash-based skip-unchanged in `SourceAnalyzer`, default-on once a graph exists. Builds on T17. CLI `--full` flag; MCP `incremental` parameter. + +### Structural Tools (each ticket: impl + unit + integration + protocol round-trip + CLI parity) +4. **T4 — `index_repo` tool.** Wraps `Project.from_git_repository` + `analyze_sources` post-T17. Auto-detects branch; supports incremental via T18. +5. **T5 — `get_callers` / `get_callees` / `get_dependencies` tools.** Three tools sharing one helper over `AsyncGraphQuery.get_neighbors`. +6. **T6 — `impact_analysis` tool.** New variable-depth Cypher in `api/graph.py`. Defaults: direction `IN`, depth `3`, max clamp `10`. +7. **T7 — `find_path` tool.** Wraps `AsyncGraphQuery.find_paths`. +8. **T8 — `search_code` tool.** Wraps `AsyncGraphQuery.prefix_search`. + +### Ask Tool + GraphRAG +9. **T9 — GraphRAG init module (`api/mcp/graphrag_init.py`).** Reuses `api/llm.py:define_ontology` (renamed from `_define_ontology`). Caches `KnowledgeGraph` per `(project, branch)`. +10. **T10 — Code-specific prompt overrides (`api/mcp/code_prompts.py`).** Re-exports + snapshot-pinned prompts from `api/prompts.py`. +11. **T11 — `ask` MCP tool.** Returns `{answer, cypher_query, context_nodes}`. Mocked-LLM integration test executes real Cypher against the T3 fixture. + +### Operational +12. **T12 — Auto-init: ensure FalkorDB + auto-index CWD.** Bootstraps Docker if FalkorDB unreachable; lazy auto-index gated on `CODE_GRAPH_AUTO_INDEX=true`. +13. **T13 — Agent guidance bundle.** AGENTS.md section, `.cursorrules` template, `cgraph init-agent` CLI command. +14. **T14 — Packaging.** Dockerfile MCP mode, docker-compose for FalkorDB + MCP server, README quickstart with `claude mcp add-json` snippet. + +### Tree-sitter expansion (parallel swimlane) +15. **T15 — Tree-sitter analyzer base class refactor.** Extract `TreeSitterAnalyzer` base from existing Python/JS/Kotlin analyzers. Strictly non-functional. +16. **T16 — Add 5 new tree-sitter languages + re-enable C.** Go, Rust, TypeScript, Ruby, C++; per-language fixtures and tests. Brings supported languages from 5 to 11. + +### Deferred to Phase 1.5 +- HTTP/SSE transport (was Phase 1 issue #3 in earlier draft) +- Real-LLM nightly E2E with API-key secrets (was a row in the CI table) + +### Dependency graph +``` +T1 ──┬─> T2 ──> T3 ──> T17 ──> T4 ──┬─> T5 + │ ├─> T6 + │ ├─> T7 + │ └─> T8 + ├─> T9 ──> T10 ──> T11 (also needs T3, T17) + ├─> T12 (also needs T4) + ├─> T13 + ├─> T14 (also needs T12) + ├─> T15 ──> T16 + └─> T18 (also needs T17, lands in parallel with T4+) +``` +After T17 lands, multiple streams parallelize: structural tools (T4 → T5/T6/T7/T8), ask (T9 → T10 → T11), tree-sitter expansion (T15 → T16), and incremental indexing (T18). T17 is the only addition to the critical path; everything else is parallel work. + +## Configuration + +| Variable | Description | Default | +|---|---|---| +| FALKORDB_HOST | FalkorDB hostname | localhost | +| FALKORDB_PORT | FalkorDB port | 6379 | +| MODEL_NAME | LLM for ask tool (LiteLLM format) | openai/gpt-4o-mini | +| LLM_API_KEY | API key for ask tool (optional) | — | +| CODE_GRAPH_AUTO_INDEX | Auto-index on first tool call | false | +| CODE_GRAPH_IGNORE | Dirs to ignore | node_modules,.git,__pycache__ | +| MCP_TRANSPORT | stdio or http | stdio | +| MCP_PORT | HTTP transport port | 3000 | + +Quick start (Claude Code): +``` +claude mcp add-json "code-graph" '{"command":"cgraph-mcp","env":{"FALKORDB_HOST":"localhost","LLM_API_KEY":"sk-..."}}' +``` + +## Roadmap + +- **Phase 1:** MCP server with 8 tools. **11 languages** (Python/JS/Kotlin/Go/Rust/TypeScript/Ruby/C/C++ via tree-sitter; Java/C# via multilspy). Stdio transport. Auto-init. Agent guidance. **Per-branch graph identity. Default-on incremental indexing.** **18 vertical issues** (T1–T18). +- **Phase 1.5:** HTTP/SSE transport. Real-LLM nightly E2E with secret-managed API key. Prompt iteration on `ask` tool. +- **Phase 2:** Cross-branch query tools ("what changed between branches"). Per-file second-pass LSP optimization in incremental indexing. Rename detection in incremental flow. tree-sitter language coverage beyond the 11 (toward the 60+ baseline). Benchmarks. +- **Phase 3:** Dedicated TS/Go analyzers (replacing tree-sitter for those two with deeper LSP integration). Copilot extension. FalkorDB Cloud integration. Branch garbage collection / TTL. +- **Future:** Merge with Graphiti (memory) and mcpserver (raw graph) into unified `@falkordb/code-intelligence`. + +## Design Doc + +The full design document (v4) is in `code-graph-mcp-v4.docx` and covers: competitive landscape, architecture, tool catalog, data model, parsing strategy, GraphRAG integration, agent integration patterns, current state assessment, execution roadmap, success metrics, risks, and configuration reference. diff --git a/docs/code-graph-mcp-v4.docx b/docs/code-graph-mcp-v4.docx new file mode 100644 index 0000000000000000000000000000000000000000..5b1064b6a51e469bd3d77c3159d57b89c5d82f6d GIT binary patch literal 24117 zcmZ^~19T-p_b(bJC$??p#L2|At%;L~HPOUQPHcPPiEZ1qIkAm5-*@l-u7~w{t*-9s z-d)|btM{(j_$kXlLcxRmXDf*W>HqiSe;3gIj_wZ5W-QA8za$9%Lt^M`ZtL=Y2;u(Q zmi9Q30}4187}>vu2>+jiW)7yVcINg!nLX@mng8=>WzwW#KM*z8km@#@hfW#3d>ujb z6e|SQ@nSMI+|SGb&BrThu7SCn5m|6*CFi9<*R%6O)AR;<3PqA#H5H3a7=Z$Ill6AC zW3zI^t==~*-n3=+t@o9je>Nwcc(zOVR00L`Hm5za`J{Dl4*a9Q-3%#12T@|F6P-5Ddoj1)NZX-(@{x1~WOi8Q38|TgAdPwz0HIpY zgj^?Kqm#X}YY)tFvi42l+CtJC$s6+_UD6D9dAJ;ThqF(>%6enJEgj>7_dyF>u<-IF8g>+5&d{FUuUbOHvHHPot zshdc$Wp@qv$OVx|Nst{ccMfztnK8%z7jG6+>?w-H7?A5WdiKcMVLfG3i1@ z(pI-jQcP^udnBZdoBi7N(wLM#bo;(h>HiQ3{kuPmbBds{?x${sx3-nnsE!Wy;|pI> zGWYFyMS7UP#7%gKISQiSR#gq5yY`j^AFh+#;II{5HXqJy!H3w};bGv?l?rOd&aY!< z6#{o=>xyV4*Rni4lNV8uYxPvwHmXvVl!l$drc154*TaOaZbKP@RJmWC-3iRS0L0N% z0Z_uc;Lv{b^%Rx&c8msXL3A1Gxh?cw%4Xq-4a?os8glVf(BX)lppwdzrcO+jPtKdb zggiKP!|sV<(I1?q*63yDajA_c3%QO=-k?uiOzw#Mn7HSA4W8cavbQVceUijmF#OQy z=9eD$9RSO6PUF}6)x~?Tn-iCT^C4BXtNWzKTQeAP&fQ6gCAczizNH+#!`s*GbhU;F z61Fh}!1TM89P%}u1FXeS%Pu>ee#QXD6@eh>_Rju3s(=6X#r{Vudo9uSU z>P;3s#AYIIJ9)F?mhWyT_?4&h$7lOKP~6;mQNply#~7{j6u?O+3Z!{}IJ2W8HN~ed5SwVX2L{~yU4ptb*amHLwqDzEyhggF1yJGwAf!eMb zRxYK`%rtWl+_DB)mlNCF@28g55_hgucHGz!LX%^Tlo9hTquE28V^C`_Veq-YcJ5GZ zl6$!iqN{Kx|2Qv)$IfZLKLiqS&e024)nf2i9?{>R`Sz_xV0X*Ie)LXW&|C z?y8ME9|qjtmH0WkJig3#Z>ZJWgV1_9C(_e^rEE|BkVuFc=C3A`Slnjl3@Lip45piM zF^&tL0*Trk@-P`V4ivbt9Be?t!kN;!7&A7#%IE9)X~@_QcP;XX;^}KmBTlexwhZA} z<;FITv^WvYtQ#_Z=OC)FI`OF8Bo~kGJiEk@dXv|JVLi1` z(0Ou`;JH|)ZY7tl@UE1{q4M31g>jw9q3J!4#_g6Lz8HDoPz6z{AK;x0w`tB7oLVN?q7>*($AO&J!5#H*D2OI8JhmI*+-dBrC9FMVWVuLD7eAtBW20whf|E)n-+al&i`G?k$Q0-U(-zieWaO;<6 zd5oHtfPZ_YUCT3P*UoNErp~?bH~IwB8HB}{@2LSpWr2Kq9n3rgZh86u_|iSo=W_=L9kMpzP+%h#k2uB5#fI7EkxV}mQ^tkVOKwNT+bqioR`ze@$z7?>2|i7~ z71|}4onS2>9jw|;mqGo^D0>CSe9Zd-o2b;C_baq$23m^;<htT}x3}OZdutM=*>*GM zO{>G=82h6A8bqM$vCFyKd=n_QhO;#u700zVP#QH%3qm08bydN-YRS>;ANdl#++a1G!+hzr?}Ig;#gz}%ai)5qEgiIqo<>3QM4G`0!*;D`#ql) z&v2>@(Xt$NS=xoHJ?YmE*~~I=q4FPYW^@+@{iP)N8EV>hP6|m|y}akdaiG5SHS{hj zXWe?iiUfUxblyayi?x?~p%Xv!b3tHJ(86sqWW-NoK-@`RU7j zd4QM&3aFljpXUmm#!-=vd+mxM-%rK4;X({oLsJ;saZGQUbn>8^1F7f-lxec5{&34k ziE`gw%`Hw)L?dht3Xh9LmsH!%$smBnCx$IG_8{2-|B$s|Q z+YRM^d12ow)=Ss3?YAfP1H#>vSxW6#S|$`H(hDgEq{DTzf^B~54wi8aaKnWqH{>&i za;m2($C8Cydzfnu;3m<%_em>~hmxbQ>j}6hieLu%8;BUWiy`w!-cQ9J5^mEi^HWbl zv+l0d0yf6PYy)R>P3NOB%^gWp_vXZt2tt7`lK$rftebcRKY{2&M}kNu_)xEHfjI?C z;R;&0Mo{sT&LX&fenUTG@;bR6H*x16Sr}m=NFt@cm1cDHZLqi0<6$^cK`t7Rv!-KD z)+j9mkQ#_c=k73@r?x~6@^k*F-|US-Y7qz)WeERRiWR~PxNYDI?$On&@a6&V?(qD; zPo)@`M-Dp&xYQAaR(7ZvV@JT8Nf6d^c1|U1xRTk<=u)+bTvs!_TMA%_3x|to1?wUf z2~7Tp3fDtLuK2;VEj=!Yo~WVdJm&wy)i&(}jI1dz+~@ISKjoPG#)f!i%&yrFQxLV1v}GCl zQp$w1K|0X;7zX~j#+X{{5_$Bb?cAOhr&mZY`&7)Fk|Yb!!-sk7XK$jP4X^)kpfwIP z81vMRHvQc)&11HS!`w0T9v80#)pYoz`s^vD8%E8QV9&M*bBBLNt3;gEHgey{*TocL77i01aHT>jS#0F~lmSAVReAdc)V%D5AjSlzs=#=vuaCWr?+4^Mkcu>OI0wNZ|76o(F))Iqfa1 zMxDZKP;YIbymM;Q$?}gTv`J5c7)52BK|`D4q2u7Z65+;L;qLcrL9TJfRY{``f=c*k zs1Rlkh_*W&m?ah_f5Me!cLSVRr?_wV3_W9> zJ7e7(4TKT3oLW(}jHL)iaL;q`KI3m(Hth!M?C&`uR(ziJv9H9t0)LbL*)Kb z1TE_s>6wWNcmrL%YNPKhvPrr7GQpS^i)mPd7ck$Jl*`m>$ zL_=c))!4C2ROkdlJh z#}(EZBB%wcx<9lDVE(qOE1PS)8uDmx6q8~_cgs;@ADz{nfTYJtk%xo3iDMe9Mi%j1 z@(RxlxjKL))etE;t1qO56TI=RA@XAU<-yx17a?&@84(ekT<09E1NxJ%dHBmB($y81 zK-&#@<=FkUncv+zklv{lVf~FP&mR6^DrlB_t5xx+PLp)wH9g9a6Ge^oz={?0dCXWE z&kP3%K7r0U^r8i`I7N|+IBp}mU<#6lWjevI!)_z2k~O^*5$4;7&Sk*E#|B|iJA^ZA zpnyFO{c-RVjftHxgr9`!H;dxs+qi;h#^zBd7D8Q1TO>PT=7YWO6Zt*lB6CJ?$x05M zY8+mk;=@D2%-$z-OBzX@A{)l(;R6$zT!f~+JSp8hyrb-}Kxhpu3|6noabjj|V?X81 zwyp96P8TN3Pt^C0L8Ws(oiyvIp4F`R*@Tl4n<-@HDdwG{NfYfjQ#P5I)DxA+%;q3Ry9tpfMJ{ zTY-aVfV-A^@OBa4gP&oBp(DoP6x=KckEDLY91Y^G8^nqCqoV^2Pza0lSY%;?;*hxf z6BJbr%i{dL4f={IxU3fk05oQ)%|To^s1b>&-rcl|N0W_zzhiH`U_NbNbL!exO+Ni8 z)O@`g_wDjdWbP&h#h$*38knL8Wk=-pmb(=e%1DsHvZb|A-pY6rGdBSvDdVFCDJiB) z_?1?fi;9ZiJ6%I`jn4;Py}zsSJTWME;_%9k7)TWC5PI<= znx_wT9k{GdL`rb=dH^GtB~4qKjzlkKuo(Ob-_tu*0D6TTrw_Pry(j#+v zgqo&e>ITNNHzrxNzCv@K;+&4O{6M4MRvv|MZ43KcKo^-`9vycs2=mhsu_vsoTR4YA zGee=O;Bmm*$Av6TZI*e-zZx*Z+c{Z;uaio{!|k67;@bC!6io2fO%=-gul^W6y?n-} zQW=(yNM@RNI^n4Zn|ps`PHKE_RB0U#nti_oCs(VOfB?=gomz(3KSV0%xJ44Bs^t-a zF4s()$YTL+eUkPOI9j?{Pnu2ON4PRkVIAu%8P8OafjR939juuKpw_Lb_!CHBWTabMWcWRwnrGDd1$yAqIm4qSw99j1tx3jI3|X$4v6 zHcI;I7Z(Ps3*Cr+B`I=qcjS3&>#XhG-)*FeSOiWUfVvhJXuKiJt#8nC30}zya>=W^ z-s|AduUIilL&XLe7%vNr;g(jgm|%$c?vwXL08 z;y)v_v2rm$b}-8?bq?MQHLx<)JC`{7-lOiF)EmdYuga(8d{W2-Ynl7*!kgRU#)Sw<#!Uub zi^+8J%cI+dI+~!6OH199qT8^S_~-B27&{kUp^CzJ!Z!SOZznj%&TUw zQVK^?VAwX^XCFK!-mp|CI@j^=6#Ar23bUlxgi!@F)9-AeQO~gXL@j8@HRVSG^|27I zb|nED7)~1Jn#lW)&r^KcscGuULt!*4Bq(1Ctv{G@IfjrLsR*iT^C=s7_fC~0>{la1ej=&WcjnSdLcN1|g zxbnuD)kwMsNVkz!i~`C9K-Y)jHR)afo7_amh^Ge6sO_m4Id$;Bsm^+K*=M++-i^$- zw>AeoqoXN)i#7Wpp%*K)@v!<1V8!CE+Z=b689?^;4xP9V-05P_tEdSYU;Nlh5=lm` z*!XpL8kE!N>vc3jAL2hNv&yw`;RKcrqqvok=lVup9AgL;GSzCrODqsgDYVb>gXvjj zT_vSS4VF-!%tX>G_?sxxw?5G^=c!%K*Z3db3`u6O_I4J@$%{xaRZs(s9&Ar*)!?}V z#|i}syHD#z9z%#sAC37m@7AXyq);;$!jA~F~=f zhpf2(`MC+Mw6{NOXI+$_i>vN&iT%*hdT18=j?W@A`$-~M!0+B>7MkB_RZ;4RmkSUs z<=%67&y-Qbgv~ii@G^LpfzTr@@&+y$f#+DS%&4rZwjl2TTx+{umFbMw8E-$L$q&B^ za(9$62tT(GbY1ZLp*VO&O zqk?2+rG4Im0vY>eM3CdhS|3Au)sH{KB?2cQ@eOeReWdLRX~6*R!4w3o3>oOa%&FE6 za+?j)x}ZwU66HP#@Mu#SBe#8nv&utfHVtc+xVArV6Pn7hZOR^@+nO%Xvc6SjC)GUu z0JSsj`G)dz!v&1m`$_p6UC%-NQu2H62?Q(xz*( zW?|0{Js-)lbp(m&6CYS3YAp3U?hGS{P+%zBg@*LJOl>`pg&NunbuxqR8Kd#s-ZM81 zaTi3f)Y?B2CxG1M_;ex+0+Vg8Yv?A%S1UO6)-$lHOOWXIxHqO&5%KHN8ga%V9kAdu z8~0Tl_kt z{1`k67JNl4O`V=Fdmb@V?&mN^=g)mCk@bZGc7-h$fkVM>8&y+iOsymQ)=RsdB_Z?ro&li{*I8@;2Tk*RAWU0QH ziRYj()+4n1rZ}$&w=x_LLw z=nWU0BqK@s$UFp5oJq&?dXV-8QZ&0p%YBe!sR=UmjB3An^(!%;^?}v zyznf}Ezi03pBD!od-dqE-y^7%z^M{RMU|B3(Cyym-qmUG?Rn6RsHR(^fP|UW@1gj9 z2w1p)k_V*1NCJ8IT2hvEc4lV>yhoe-x&0Vkrd&f89QFyrND?XQDCSVZ+1qwjh1z_;u|&9-WxZ4BrSsMkFQxnUe+kgmM z*38^3HoQM`hRBnEZW7JcLri3>U}yU@p@iWpt>NmYSzu-(C3FzuBMS-PxDp5qq1 z)4Q|Zp-Rkgg3r9DLxi^g=HR6b|SIbch7&PqH5UA@BRAtZd4L+#clah1aU zS({^=JHJ9|f2ulLzqfiohU3p1(;caa`He@BWk@iij~2&xm|tN-9(cD8(z*n#i!?%* zyQL{`XrOBgkmLAwsXad|tgm(bKez79{n@bA5cbR$F{lZ@Uo#TaLniMZodYUuwykVn zd(({GIgRvZh#`RBEh%7v#7_5*PsGOVf6)=zF+#hm#BI!87EkPAuewV7JSSQ0uzUgq zK6u|AKBt?$*1ZwqnmMc~CPnqfs*6DgL?wc{x#LeM9~W z_uqoiS&IH@eXLZBG1fwgIvFV3QJl(RL;;4xLjY4ymgEyy;16?s4}j8Zb?y6%sHI?8 z(Zl86O|f)JinLTo=q^-w%yh~q18K=(f=8UQiEMZPX!_l-B9VzI!DSU}4|X|PNdd;Jd|(sTCV zUP__|b$>(G z0~7)b?d5@aV9DzF>e%k@4;WrA!ErP1e$cv-lF zH7z9e-9pWjTbPc1y*UK1j~CT4T&bQN3~L+E7bRt&zRaz z9eJ?M!i!R%eHXRxiI-7NnUOi^kXAL$azDSviS|U5{q}8gTH<$-g~daDmHAAI{BLuL zqN#9VRFjA;bYDQXcp#uZc=t{DkX+<~t`1%*b(;n|!-WTY$d(gDxhX=x5*$VHV9*Ed zJ{5Z*Rw=|_3@Q;MjBEz1&rGPJL1iWhy1uk^^RnTK{b7aD+H|{X77*7;IO45~e~tY; z_Xi_c{jZS?Chw+s4tPd1a*ildW@s9^Z@J3gAz8F#5DN3BLqF8g+2XYYjLQ-t%o%(w z%`l38R1Dz5wI3W&y`%d^5MccKNcTM~jJ&Dth@Pgri~=2y$a~t6pN3HvA3YC7#%LM} zbQOD#xDwvjsJI6u2{T#!&u6UPG{99}DZhefP<}HtKB?g|EGm$F@c@&Z@9s%3l>oI;nDyRtFbB zHqT!Tzp-phmLaLjz;R|XD29DwmX?3KWM)+^4HsA=qXHK^@r~lnO6sd0jXSUsBY?_L z6D4aC$nggSEZU45_y>D^Y;fJaQh+7Q7u4Vbmt@8KI6&ye0%!=AF!3Ddc$H2G-AKSl z^_PQ6{r4k${NnHk`T-PasY3P5mOz^?pi5Nt7SFM*>lKDU*NTj1%XPOu8htJyNybV% zO8#j}6HiNgT)9JrX$VFxblgVsmOtnw1R`~5Ka9adG0kE2NYdN*`SHm8AkbDn86(AJ z?nbHKGF+=QtNS7m&ffp+p9wxaRB)ki7aM498UjHJqba<6Vq3}N5jQL~gUGyuvt#n1 z+2T>?(Jrjxu(eG5wcRr zoJ-g;Hx5aTTSfZYhVP!BPk0f5H(xcEp*W0N*K))eIzfdIL4VvdN2z*0ljh7$%*#^SH9oWLW!=YThyy-lC&Y=@ zHaOS4fd%$1&FK}ergjpF1eec+9>GeQekfss_Rr|%-%vHk(Q&kKI8gb!n#-7gpDb~n55WeW1ka~ zB$5Rh!R`DBC0~PGc~rR1y)4@No7@;K(uUd7%e)XqvSn-<+`q}K+aHG@Knj9&q~M3> zL~5HTbUVbL)(l5w%ES{it6|^|gUZX)I7Dr>a(Ry`)SS+M2l_VFD1;6zx4@%KAOgw%))`W9_KEE6)I5soH42Q+O)b+A*@2tk&+Cr-J zdMXfh>Km(bhM;DcjRV>;^|i*dDsh{cW=q0Hmd-A@NQ5|sas})H3UltYEd!NO(9WM)Vd>})@wN6q~1HYbxqE3cnpxopORIAmli zO0EBxV0UEmGwOwtgy~{`{}|d}+oD=oNSd-`J>BL#js|)&80XoSHz9Cpj4q$akg;rm zR?U}<35Lzd$rmXF9%*@Cs&EBI1_)~P9pEZa&rLeLlVz(h+kzgO)kr2ZNIFeK)pvkW zD_81s++>w669n`$!T^h2e{&vWFpXFKyB_$%hp4l@ika6Sixgfyu8QF|>x3Ql(gtv9 zhgu~U)>4s)+x;v>wADUD$vh>5JSo6s{VrH4Q7F$-?>t&MI0gKSIF}=miLRcYhC(m^ zVw2pM{%8G944RhvFuNX;?su=!1Vab#-6pnSI@cNgy1 zu4qsK5licojDiUiZWC6o@FYiD#Pg+BLzWx^vB53qZY@z-qWmNKBXm{g$WMXE=RhJU z|A?#gN0I>g$ z%8vlDLEYc*GA-XQ0ON??GMs4Om*;vskFV!Z6|D*!csnL0y)GBL z$S$qQK1tyq{Nf+8Paa-%sI8rJgEXfe0=2CCxVgP6`TF~ZWmqvX+qu^c>e>$3jF#Mr zX8w@-7`7)zjd_XFaX80~n1i@>;^J>mkbNvn8$s6Sc=L~$Dugvxk#^>;of@g#god*~ zRC%~AmO!gzXycYk++qV9s2yF%G&h4dnoDkV$|F?oB2KQG@ag7620zd+ zx>&gf!?7Zv+?e$F*f}Ykryu>&*dNRshy-{qvL_29k4ft^z0JaVV9GMe04vGm{}YWe z3CxTYyh21>Z`R2@+@Y$HO{*lepu0j;D7ThUGelLO3SU~>dn8W|nT%({t;|y$OH_2M zV$b^DskRfl8H4=0Vqk8xbJ7Zq$${Z4`Rz?7)HF;AN99Al&EX&D&*hf%Po61I6xi21 zhtASjDZ;uUab2K=xFX;a8`5Ky`$z+>tSPHp!B2oNfv~=*1bB1s7Jb2!$SMG*7@tx1 z4!aD7&xXk@3pxN8+KHh#ZiGhFb(!-vGmPANr1& zgro0qKx1bAG_A3M)Y)vAIk~uw;}s(1c|sYzUM5d;T>wB>GeJu*Bf$1+RoY` zQG*ou%{eiH~GLu2uz}fBpyCnQ}MpA1b<$TdqfB(dUF>Zih2`_WsSkr4I|GS`gK+P9 zxlyP#nGLV$2A<;wh>_H;G2YTIL@o|}3g`WHFc(hG-MT~z4=78D)CjQ*UG0{a;F8xy z%f4U)By1bN09Ms9#zWGHvN#75JQqkK`y54(v$XLKi~j|z@E~n0#RT_L#YDJ6&k6A8 zl?=xG=4EFldFS&;%O|jlXx#)LIb1i!wmDRx7Q#zZD6R;uNP(}tYnTZ3{u@DryxK+x z7p)Vs$sTh8MYR0HtUG1PYe^t4q2h{c0plAJyHPkhEhkO`)~>Z_{xwehjHzSBMI)f0 zr?oi|4alZQv7N3O1NdH3(R-Dy2(lhto5FC3{4s-=wA2@IoFLJK0mfB)+7};H^Z=Em zhs}%gy|QP6VUY%rzzNt$q+hqXmSOQL7q(6*ty_@gf-tmLp5{QgsqxWUa#@Gk7dqTd zbe2W;+-xY6-H7aVB~u zduCvrc3Oz57h88eWAb_&t|Kqftb9{nSd-s%bkZl>OS~a4LWSh zNbSSG^p`VUmKYZp?l-pG>wUz~V^)M<>EGCQ4($*%CSXa7!j6?KeAX$DI=s$**hvR^ z%8dh)%O=DV7qR&k;7h(Ptx)r8-^FUsR#bK%DQfy4mmS2VjXICvMyDixL!=!q!y6@$>^TL-O3ib0U_giaLbS7fVN4!!*M1_Sge3d^mY@WlbJg`$6_o)sD46I7n% zzI&1YEQPz)syNrWu=M^?PIqpMDP81C1s_+QtZg?&(C8W&9(gSKObiN~%)O)MD+*s zD8cOz8AC9USX%Bo56+^8Rs`*WPZta)v+J2iiV)!C8riIzU4Bkq#kzgZ?(*^T7CjH> z(OvdIX1Dp8S`8mXM<*82-*ER_g))9+F#VQyDW2G2P$$rnU!I6W$6g(Z*(Al=GpTKbC|Ja|dAFget{ zpxl)l{A%m!{4zJ8&_7PsvLceVPai#=+_N9ver7s$Xaik`)NvVmgB}ZzdSPsxnOTlE zE{@($N~{ll3HJEV;ML;(dUxrE?n(MD66p-VvzneZp$2INU4#?R>wRG&sn_x^anJs& zevC13fEaF5L!7%6KYV{Fg8Ba6OLMw-Pl>w(E_``B{o34X>fad9U?J=bkR07J>QL|? zkHq1qESLuwam~?%q`mn_NxyzESA5lG19y|#WztTwIsmXO%aADw&r}ls#J~cn<$jEF=!#hpl$89X-079h%0WjURn#M>x~_8f z_~pDHz*9UK2etGq)67)tuX9t(AB6}`)HoX?vpv*-0ERx~a9mV~3M)F4R{CT*3`Kd* z{1%W6wEeVD@9TOKPe?@36Z+J;shG#TGiU= zcE+AcR;HfFU@)RibG)>5S+Z@^{k-%0YWC}B=Uz?}w$6TFvrGCMnhlnv)U4Iq`QC&i z{e!HaNoVWUacAZ5f%PuDCA{&XM10k+zZ=wNL9q-r;mX?OYI^X1h!BC7QWWB^pN@6f zim{b7`#V13Sh!ZJ)b*tRH-<;?lS~AfkcPx&rS$;kWX=Q&9&-7)gis_AU~noaFXfm^ z^NcsY{tt4NzJ<{JRL!qEwsjep`Wg7s<6Wv3dWm~kf3fcvhvmakpG=l<0hRyH(Hu4o zxQx~~*6`^S;td5PtAb>~3!XYF`(|3q80&EN%c-80lyzfTag zs17uvD)pZ78W^!hA(W1ar=54O@5oDk|B?fg_If93yh|12)=#$&$ zWalu#=rf89M8hFhSq8HKk&4`QJ%;-JW`s7#K9dHDxiE|K*DxX<(?Yz*4TVda3+q4y z>(`}Yfj_1Vz{W|kqI!VD7*kSg);l*4*9YN|AP{GvLu7)(rMH`L;fo|>p9zQgo10b! z*8KteD0lv?T#NNk$D)WppoXrX@-$Eo?JBnc&>qd3{%E z9sv+>!GRT2?dHGAwEj*9qxVJ+f;ooY%n-679^6ENAJ`y3wct7-E{Wfv?GE}fn7>JY za)o&~47gLHUWzB?VD-%1_e_#6=wEUy5;&+*YUT5Bfk0Ez90n=X#YQ2Ht6De}_$+_8 z!()rNg>1T=kuMqNNtD5&$Lu^y6`C)089?(Ac~ju(g@&<39cNmZSG#D7sIW$|nn*rY z=sY#LwAdKDFLP|ioqhVKB2r>DTjEu~376;{(^Ow$ZW~*Y?PoA`Hsj!U+4lY z$XOfkf@Qc_8w_$q9FK`gEuKvb*=97^Ck%9FrDLz?sV70Q#jUeU!?9$aPV*M;)u^_c)Nj}(x7y|%GYi>OEc1O5t{0XB z7~bPR^Sb=8Ct?yCq;yk~j~t3yQaeuSz|9NSp;r@G=X~pkIxlyxp0jvGTC8CBpJ5OK zX5BiSYmlA&MuRC$c`lf{4Cim*wh_U$k%P1Y^?MvS()c`PJlJmwk};p(Ej-yB^BdL^ z6b{9i4Y~PJjJJi5YcD}2V2e+ZTnPbYsum%E6tR-0<#gEY5%}Q36r*ja=)ae?3@>t%my5u5diA>5dzBw=OJ2eOe}K zN`6N!TWVIdJX`RoW_z0hC?yi*zUe@UhvcdJwdcAQsCu38RodDan@-LDTZFIS38Sqb_|6yyDogEzi-+<;io%TDve}Lv72(bU+aQ_dPse`lmeR&KY4xCrvBPHgs5|Fa zqk19i>|j1Fm6rJyG0IbHCPSk0&HSE>tnjav-o=np@~KP2m+r8cQKvMh>g6ICDOukbl$>DOpjhG z-WHWo;5f`R>g(uO_$=1lFq*)Vf0TCk+8!!=yC4HKB3hTz1}%qP@5Adb`12c>vzxB- zce?mdBz#^Zy8#@|6sA{-nkAX7F5O@F>9H=9tTjxq3aXXLf40phR#&6Z=iB`4`11RT z_2|4>P8Fw1@GEfO3nid7&_m4C6S;YYe|99TfE|Y8hmJ>(kLocLYQp3#7V&bz4=^sZ z{mfIq%g~*wb+vMp^}~v4u4A9PNm&d_j8gq$Gr-zaA<^CKS2=Asyht}V-`Mt^cP{E- zV)n=bfzxGtkA~oSjnu<(2s7D+u=@pYZyay1uX*k_^Gqv_vXatwN1cbv~4rm ze-qXZa~_n^K`7P){^l$9U2(=nweep)t84wYHOpGxXMxNjvRwaggXTe(V+w61p@7ZSuv%!bKoCeK0!Ug4xv=Gf^(kh3K?E&*b)Wvcv z?mb+R&k+!kGIjOqC8tvHS&5P*J& zpmw#3v?M-+ew?Tvy&k zkyF`*J1nNdQhdkXn$e^Zl$PUhO-LuC1v&N^}wgM9z1R(oVW#4|3pg~>5wKBD6OcW*BjKQ{ya;VQ_PK@v`t~b@6<;EWpMY$nlMPmMdyyU=g2_F zn(ixX_zD{HM($Z+SJF?!^mq~uvZx8wRfMf?^PU@#8^vRrB|jjF54L~p0AB8KW)bD1 zTdQ#JJ{ywgy+!z_Vq|us=1Z>s# zCjF{HLhhEZzqBS=5t9|=@jm#ZiF&ryr1Ki?_}1lf~n+G=Z)DRkt|>F;=K>^Nop{ALafu zyQY)y8EPb2|9*WzLvsNDzvw|``WsIqwxS0)>C#^EM~e2%+|gfXy>3a?uSNssq|%%| zfB{i%n&A*pgwO>fNlacVx+Kd`k6Z~Q&~orAw|J9X!i3!$qsc1t*zAdA6SH!;i$V*O zv$wf4lFv%8gva*$YjT3q9mK~WMmI)32?sW|7`pMz09wlKWZ(++TaM~p&&Jy%ADxa|( zxcZt*b0TVET@VljhJY|HNN-%P1N1)c)Njw`MKv3g>p(O{jg_3jt-|ZJqB9B5k*(=vUsN@5>=> zkQ}-b(})+JaImJOf~YXFBDf`6R#TOOXdYn(2@WS`FXrhBAlG@^U}@-vemZu=kI4Ll zZJev-yH>E*i(%3&ki|DP)-SS#gq8vfbQr|MdB=P`dMP=5j8vutAfUa*YN+MQnXj_)O8qr!ks|8XwJV~BvcLN?=%6wiAgSJEe zx%v01cVkyiL4N3rMpG#p)x_UuwTB?)TJ@ux6HHt3*+vE(Y@OCfr`mXxqb|-FCx^Sd zT9#@XBr<;U%@vfq@fy{CXbloLgVDCeAipAwD>%2 z+`tgDBBKD!S*;sSJA(@OWURgK7(Dj|AL?J_j*5~7UKup*=7u^iJgD>15w){*f=&<8 zS9P~Han!xI1!~lrZ6Pcerv$B|lm=IBEB zdv5}&B2rBvrk!cRLRL*_k^l*}!n60tZC@9*LX?(JN$xQ0GH?}tSL!ayorOdymX79D z7EU$fF+b%4c~rz0g5ozJt+9jjXGz;Y6RXqbQM22j9iQ+df;34u|1}`<8f7HU%TDC)L}63}Pv?EqZlbN540JI!k`yhQd*dDN&8Fx^)L=C+Xg#YQyQ>m4~~3=0z?uTap>dWl&MUkBrKEZ zcqk#$JkK_iV@?A0XZoxG=vh%6HAB)tcACc8MEOuYR&uESDeClO?rTNoY~eS!4WtHj zMfE{nu~KMk`X}MKaL=~T5avf!YZyAAYwp59({6#CA70ZIm*;sVG=Vee1(@Rw^IxyD zrhjSoE~ZVsp1BB}9~AV?^yzj+(%34*wKQ6t`(86XjZPj7Zh+|R-ZBdBr-IB+quNkR zW%8O{_^$cW*QPipXaJgpa0G;XH{H$Lg;#K_6$-!B^!MDWnaAQ&HRuvnDPZhET5pPT ziZfkisluw;AZwju9b!|YQaD#cWWpd*pXRtx=fqo-^u%fnVL2Y@kTDM9iE%y`3I=`- zh%VwqJ57OWm!vR=+pW%VHvH~>6Y(86LaK-K_?Cb0 zgx%?>N~}O4x9}4Or$I~Ar^$)AW==Vha%5kn4!9PDgG>i|eBP-gXV03B>LZp2Agq0R zwaE9td`&JUVIaK+(zbGM@1|yDS7+g2cbm19=_ayey5Kqn_g;|_?rY}eBKlK1`S6U? zobJ}CEdQJFJisR0iD(mH;OEEYfgh_@QS691QO%o*C^lU3!IjZoeZ|EVn}a))T6oX; zdy^Hu&C&PO8k8R`-BCeue_^R7MoaQGiRyvGoTWhcF`jKSKWV=(dm30kKEV%dkfW8U z>NL^g2#@WY!%qd8@*niVIat_8n-of4m`M8y2maTwwxCRSSTmHR(NLED8H8qHYy5|# zy|ED6ISQNr{q|Xb=J5LA55YqrWa{AJhn9Y6)V!`Ix*Qd>6f=EYR#wpO~R4vuh$xDMT&Ed zeV*B#KWf(LUQ&Jv;G)P6t;pe;qX>Ywj8;{?O8nauJDKWF8S&;yR>y=zGR);E|&0RK|aCH22Qi?EyNo4&Yu_=Mv)Ih;ey_CceGgbehxwk3)#q* z+0LqKU$A%$!f|bt)I#r-qDN?Y{Iu}k#fTfQ^LPuxh3c=LNlRFEFNYQy(oU#0g!NZTU3>(;o zVl`b9hX5&xz=Y7(sPQ@an6df_MXaCv@ni&Kw!~xZZEr2Y(Jmx_r>z}o&kj@=dm7o< zTuiL|Y0poS4zm=5$kRF?ZjES773fFG=+oAB+#okHpS5tDU}O$7=s%>DPDoSmm0Y3u z5|c36)H)>15kh@Go=MBkGtLJoP80X;{(U#^WAKEpzq8a|pBSXA?DK$p`~uYk>LNIbTbi>YN4)rrbJYclrMv1+`vTl*q45Re=tYr!rj^Q#jq!+_9i`hMa=Yhy(3 zW*ZCwG^O-rD(FA?TNgdoEkdoA_4O>08qbkF*LsvhM^NdML04*IkwPb49@B)I!x48j zt3c$FdTz0&M6vDOFN)*aAw;N}WE8Qg(&@Uosy1iL`8nPIDV#fNB?u43jKv6De<1~pfXv1!1V>-uD zVqp;vI}(78t7Q0KvkALn&r*nQt-o&uuU5kT!_+$HoOY-e@0S8ZO!Lj#`q?mL0<1!hi+Q#BVA}ukbv<lJUs3;+So$ zAK_-he-rk=9O?hU=+_YN0snd|FEmDju8U0LV*MJ@`>!Dv#pIvs0zwUaU?Myv+26pQ zmoEI+-d{-Q7rCoIXp(bvh%dY(__?*0lH#Bg{-FY_40t$voBa|_%kv9<*>t}I|GnOR z(bD_5F1EkN2a}&&!hUWZzQy+&2mp}s!@zzw{NUm6eXC1&pujJcuXM34@qgDE8sq+2 z7g}ox|LzA|Vt%R&-!=G+0sy@JV16(d9ti95F6!%-D^%nPLtyH}|D_-{(JO*r>V5wx z2);^vsnOVPK`^?&?+3obc!}1M_@xGn0`O?~BGV;0PU;^^PVfTY%NUmeoMd3B^QWi* zF95!Xa4CRG7Dm9Yk^(#$K7+qRA3)JRUbZW_96TPD4Hr4!<=T_~n-TvcT>ofB_%!WO zRJy`1QLy3rztiO6D!yFdN>?<274naYzz61+lI|=2k^~#3!z%(G2VTOFRDQu>V*_|N z?1^68$D!(#eZUI&$9=%x`j?7is{N8=3N4BLi2?qdCRc5{u6{+6D^h-`g7m`?-2&VK N1Vg`z)42HK{|EGzrR4ws literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 121c5b9f..95eb07d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,12 @@ dependencies = [ "javatools>=1.6.0,<2.0.0", "pygit2>=1.17.0,<2.0.0", "typer>=0.24.0,<1.0.0", + "mcp>=1.0.0,<2.0.0", ] [project.scripts] cgraph = "api.cli:app" +cgraph-mcp = "api.mcp.server:main" [project.optional-dependencies] test = [ diff --git a/tests/mcp/__init__.py b/tests/mcp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/mcp/test_scaffold.py b/tests/mcp/test_scaffold.py new file mode 100644 index 00000000..7f4601a3 --- /dev/null +++ b/tests/mcp/test_scaffold.py @@ -0,0 +1,55 @@ +"""Scaffold smoke tests for the cgraph-mcp server (T1). + +These tests prove the bare module is wired correctly: + +1. The FastMCP ``app`` instance is importable. +2. The ``cgraph-mcp`` console script spawns a working stdio MCP server. +3. A client can complete the MCP handshake and ``list_tools`` returns 0 + tools (no tools are registered yet — they land in T4-T8, T11). + +When tool tickets land they should ADD tests, not modify these — these +guard the scaffold itself. +""" + +from __future__ import annotations + +import shutil + +import pytest +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + + +def test_app_is_importable() -> None: + """The FastMCP instance can be imported and is named ``code-graph``.""" + from api.mcp.server import app + + assert app is not None + assert app.name == "code-graph" + + +def test_main_entry_point_exists() -> None: + """``main()`` is exposed for the console script.""" + from api.mcp import server + + assert callable(server.main) + + +@pytest.mark.anyio +async def test_stdio_server_lists_zero_tools() -> None: + """Spawn ``cgraph-mcp`` over stdio and verify the protocol handshake. + + The scaffold registers no tools, so ``list_tools`` must return an + empty list. Tool tickets (T4-T8, T11) extend this expectation. + """ + cgraph_mcp = shutil.which("cgraph-mcp") + assert cgraph_mcp is not None, ( + "cgraph-mcp not on PATH; run `uv pip install -e .` first" + ) + + params = StdioServerParameters(command=cgraph_mcp, args=[]) + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + result = await session.list_tools() + assert result.tools == [] diff --git a/uv.lock b/uv.lock index 42d272fd..2f57f302 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + [[package]] name = "ct3" version = "3.4.0.post5" @@ -317,6 +356,7 @@ dependencies = [ { name = "fastapi" }, { name = "graphrag-sdk" }, { name = "javatools" }, + { name = "mcp" }, { name = "pygit2" }, { name = "python-dotenv" }, { name = "tree-sitter" }, @@ -346,6 +386,7 @@ requires-dist = [ { name = "graphrag-sdk", specifier = ">=0.8.1,<0.9.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, + { name = "mcp", specifier = ">=1.0.0,<2.0.0" }, { name = "pygit2", specifier = ">=1.17.0,<2.0.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, @@ -614,6 +655,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "huggingface-hub" version = "1.6.0" @@ -875,6 +925,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1136,6 +1211,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + [[package]] name = "pygit2" version = "1.19.1" @@ -1189,6 +1278,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1244,6 +1347,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1510,6 +1635,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + [[package]] name = "starlette" version = "0.52.1" From 408efdefb31ac661c12211614b4ca102677d2de3 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:04:42 +0300 Subject: [PATCH 02/63] fix(mcp): address Copilot + CodeRabbit review comments on T1 PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix stale entry point references in design doc: api.mcp.server:app → :main - Remove contradicting decisions about tree-sitter/incremental indexing scope - Add language tags to fenced code blocks (MD040) - Add anyio.fail_after timeout to stdio smoke test to prevent CI hangs Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/MCP_SERVER_DESIGN.md | 14 +++++++------- tests/mcp/test_scaffold.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/MCP_SERVER_DESIGN.md b/docs/MCP_SERVER_DESIGN.md index f1b9c1eb..38b2f077 100644 --- a/docs/MCP_SERVER_DESIGN.md +++ b/docs/MCP_SERVER_DESIGN.md @@ -15,13 +15,13 @@ Phase 1 also bundles three foundational improvements to `api/` that the MCP serv ## Key Decisions Made - **Mono-repo**: Build inside `code-graph/api/mcp/`, not a separate project. One pip package, one repo. -- **Module path is `api/mcp/`, NOT top-level `mcp/`**: A top-level `mcp/` directory would shadow the installed `mcp` PyPI SDK and break `from mcp.server.fastmcp import FastMCP`. Entry point: `cgraph-mcp = "api.mcp.server:app"`. +- **Module path is `api/mcp/`, NOT top-level `mcp/`**: A top-level `mcp/` directory would shadow the installed `mcp` PyPI SDK and break `from mcp.server.fastmcp import FastMCP`. Entry point: `cgraph-mcp = "api.mcp.server:main"`. - **Python MCP server**: Use the official `mcp` Python SDK (`from mcp.server.fastmcp import FastMCP`), NOT standalone `fastmcp` or Node.js. Avoids language bridge. - **Reuse everything**: Most code already exists. The MCP tools are thin wrappers around `api/graph.py`, `api/project.py`, `api/cli.py`, and `api/llm.py`. - **GraphRAG SDK powers the ask tool**: `kg.ask()` does NL-to-Cypher. Code-graph's `api/llm.py` already integrates this — repackage for MCP. - **Reuse the existing hand-coded ontology** from `api/llm.py:_define_ontology()` (lines 26–233) rather than auto-extracting via `Ontology.from_kg_graph()`. The hand-coded version has richer entity attributes and descriptions tuned for code. Refactor: rename `_define_ontology` → `define_ontology` so the MCP module can import it. -- **Ship with 3 languages** (Python/Java/C#), add tree-sitter for broad coverage in Phase 2. -- **No incremental indexing in v1**: Full re-indexing is sufficient. Deferred to Phase 3. +- **11 languages in Phase 1**: Python/JS/Kotlin via tree-sitter (refactored onto a shared base class in T15), Go/Rust/TypeScript/Ruby/C/C++ added in T16, Java/C# stay on multilspy. +- **Incremental indexing in Phase 1**: file-hash-based skip-unchanged, default-on once a `(project, branch)` graph exists (T18). Full re-index via `--full` (CLI) or `incremental=False` (MCP). - **No Graphiti/memory or raw FalkorDB MCP in v1**: Out of scope. Available as separate servers. Architecture supports merging later. - **Auto-init for zero config**: ensure-db auto-starts FalkorDB Docker, auto-index on first tool call, auto-GraphRAG init. - **Expose Cypher in ask responses**: Transparency for the agent + learning patterns. @@ -34,7 +34,7 @@ Phase 1 also bundles three foundational improvements to `api/` that the MCP serv ## Directory Structure -``` +```text code-graph/ ├── api/ # Existing Python backend (FastAPI, analyzers, graph, llm, cli) │ └── mcp/ # NEW — MCP server module (under api/ to avoid shadowing the installed `mcp` SDK) @@ -69,7 +69,7 @@ code-graph/ │ ├── test_ask.py # T11 — mocked LLM, real Cypher against fixture │ ├── test_auto_init.py # T12 │ └── test_init_agent.py # T13 -└── pyproject.toml # Adds `cgraph-mcp = "api.mcp.server:app"` and `mcp>=1.0,<2.0` +└── pyproject.toml # Adds `cgraph-mcp = "api.mcp.server:main"` and `mcp>=1.0,<2.0` ``` **Note on test layout:** Each tool ticket ships its own integration + MCP-protocol round-trip tests in the same PR — there is no separate "integration tests" or "protocol tests" milestone. The previous `integration/` and `e2e/` subdirectories are removed in favor of per-tool test files. Real-LLM E2E is deferred to Phase 1.5. @@ -276,7 +276,7 @@ Each tool ticket ships impl + unit + integration + protocol round-trip in a sing - Real-LLM nightly E2E with API-key secrets (was a row in the CI table) ### Dependency graph -``` +```text T1 ──┬─> T2 ──> T3 ──> T17 ──> T4 ──┬─> T5 │ ├─> T6 │ ├─> T7 @@ -304,7 +304,7 @@ After T17 lands, multiple streams parallelize: structural tools (T4 → T5/T6/T7 | MCP_PORT | HTTP transport port | 3000 | Quick start (Claude Code): -``` +```bash claude mcp add-json "code-graph" '{"command":"cgraph-mcp","env":{"FALKORDB_HOST":"localhost","LLM_API_KEY":"sk-..."}}' ``` diff --git a/tests/mcp/test_scaffold.py b/tests/mcp/test_scaffold.py index 7f4601a3..72bb8d05 100644 --- a/tests/mcp/test_scaffold.py +++ b/tests/mcp/test_scaffold.py @@ -15,10 +15,13 @@ import shutil +import anyio import pytest from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client +STDIO_TIMEOUT = 30 # seconds — prevents CI from hanging if the server fails to start + def test_app_is_importable() -> None: """The FastMCP instance can be imported and is named ``code-graph``.""" @@ -48,8 +51,9 @@ async def test_stdio_server_lists_zero_tools() -> None: ) params = StdioServerParameters(command=cgraph_mcp, args=[]) - async with stdio_client(params) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - result = await session.list_tools() - assert result.tools == [] + with anyio.fail_after(STDIO_TIMEOUT): + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + result = await session.list_tools() + assert result.tools == [] From 6dafb5812151e9df76257a776a76eac27daec33e Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Thu, 7 May 2026 15:07:28 +0300 Subject: [PATCH 03/63] chore(mcp): tighten T1 scaffold per review - server: pass transport="stdio" explicitly to guard against future FastMCP default changes - test: drop STDIO_TIMEOUT to 10s (a stuck handshake should fail fast) - test: pin anyio backend to asyncio via fixture so transitive trio installs cannot silently double-run the test - pyproject: add anyio to test extras since the smoke test imports it directly (was previously available only via mcp's transitives) Co-Authored-By: Claude Opus 4.7 (1M context) --- api/mcp/server.py | 2 +- pyproject.toml | 1 + tests/mcp/test_scaffold.py | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/api/mcp/server.py b/api/mcp/server.py index d81111df..f0ae6f84 100644 --- a/api/mcp/server.py +++ b/api/mcp/server.py @@ -19,7 +19,7 @@ def main() -> None: Console-script entry point for ``cgraph-mcp``. """ - app.run() + app.run(transport="stdio") if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 95eb07d7..5cdfa914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ test = [ "pytest>=9.0.2,<10.0.0", "ruff>=0.11.0,<1.0.0", "httpx>=0.28.0,<1.0.0", + "anyio>=4.0,<5.0", ] [build-system] diff --git a/tests/mcp/test_scaffold.py b/tests/mcp/test_scaffold.py index 72bb8d05..851f091a 100644 --- a/tests/mcp/test_scaffold.py +++ b/tests/mcp/test_scaffold.py @@ -20,7 +20,13 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client -STDIO_TIMEOUT = 30 # seconds — prevents CI from hanging if the server fails to start +STDIO_TIMEOUT = 10 # seconds — prevents CI from hanging if the server fails to start + + +@pytest.fixture +def anyio_backend() -> str: + """Pin the anyio backend to asyncio so transitive trio installs don't double-run tests.""" + return "asyncio" def test_app_is_importable() -> None: From 0c7e3dbbc3656dc74e89ec28f7168b080db4f556 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Thu, 7 May 2026 15:35:21 +0300 Subject: [PATCH 04/63] chore(mcp): regenerate uv.lock for anyio test extra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, uv sync detects pyproject/lockfile drift on CI and silently re-resolves the entire dep tree to newer versions (uvicorn 0.41.0 → 0.46.0 was observed), which broke the e2e playwright suite. Lock now matches pyproject so installs are reproducible. Co-Authored-By: Claude Opus 4.7 (1M context) --- uv.lock | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/uv.lock b/uv.lock index 2f57f302..c62df38f 100644 --- a/uv.lock +++ b/uv.lock @@ -373,6 +373,7 @@ dependencies = [ [package.optional-dependencies] test = [ + { name = "anyio" }, { name = "httpx" }, { name = "pytest" }, { name = "ruff" }, @@ -380,6 +381,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "anyio", marker = "extra == 'test'", specifier = ">=4.0,<5.0" }, { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, { name = "falkordb-multilspy", specifier = ">=0.1.0,<1.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, @@ -1775,6 +1777,14 @@ version = "0.23.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/dc/d4a0ad9e466263728f80f9dac399609473af01c1aba2ea3ea8879ce56276/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c", size = 333661, upload-time = "2026-04-14T15:11:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/61/7a/5c862770460a2e27079e725585ad2718100373c09448c14e36934ef44414/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c", size = 376295, upload-time = "2026-04-14T15:11:15.346Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/0571a3a34c0feda60a9c37cf6dd5edfdbc24f8fcb1e48b6b6eb0f324ad2a/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5", size = 358331, upload-time = "2026-04-14T15:11:16.418Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/0f7e1f50f6365338eb700f01710da0adc49a49fa9a8443e5a90ea4f29491/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf", size = 359444, upload-time = "2026-04-14T15:11:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/129bd56d5ef22b4ae254940a09b6d3ed873093218868a3f9635d571d514e/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832", size = 358143, upload-time = "2026-04-14T15:11:18.755Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cd/e12cdca47e0c56151cb4b156d48091b7bc1d968e072c1656cf6b73fe7218/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e", size = 357524, upload-time = "2026-04-14T15:11:19.717Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/f742d60f818cba83760f4975c7158d1c96c36b5807e95a843db7fb8c64b7/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be", size = 338755, upload-time = "2026-04-14T15:11:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/8a8642b9bba86248ac2facc81ffb187c06c6768efa56c79d61fab70d736b/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345", size = 337261, upload-time = "2026-04-14T15:11:22.111Z" }, { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, From 4a05363a14070ccd8ced63694445bde3d11d3b0a Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Sun, 10 May 2026 10:07:28 +0300 Subject: [PATCH 05/63] Revert "chore(mcp): regenerate uv.lock for anyio test extra" This reverts commit 0c7e3dbbc3656dc74e89ec28f7168b080db4f556. --- uv.lock | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/uv.lock b/uv.lock index c62df38f..2f57f302 100644 --- a/uv.lock +++ b/uv.lock @@ -373,7 +373,6 @@ dependencies = [ [package.optional-dependencies] test = [ - { name = "anyio" }, { name = "httpx" }, { name = "pytest" }, { name = "ruff" }, @@ -381,7 +380,6 @@ test = [ [package.metadata] requires-dist = [ - { name = "anyio", marker = "extra == 'test'", specifier = ">=4.0,<5.0" }, { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, { name = "falkordb-multilspy", specifier = ">=0.1.0,<1.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, @@ -1777,14 +1775,6 @@ version = "0.23.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/dc/d4a0ad9e466263728f80f9dac399609473af01c1aba2ea3ea8879ce56276/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c", size = 333661, upload-time = "2026-04-14T15:11:14.227Z" }, - { url = "https://files.pythonhosted.org/packages/61/7a/5c862770460a2e27079e725585ad2718100373c09448c14e36934ef44414/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c", size = 376295, upload-time = "2026-04-14T15:11:15.346Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/0571a3a34c0feda60a9c37cf6dd5edfdbc24f8fcb1e48b6b6eb0f324ad2a/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5", size = 358331, upload-time = "2026-04-14T15:11:16.418Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/0f7e1f50f6365338eb700f01710da0adc49a49fa9a8443e5a90ea4f29491/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf", size = 359444, upload-time = "2026-04-14T15:11:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/129bd56d5ef22b4ae254940a09b6d3ed873093218868a3f9635d571d514e/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832", size = 358143, upload-time = "2026-04-14T15:11:18.755Z" }, - { url = "https://files.pythonhosted.org/packages/7c/cd/e12cdca47e0c56151cb4b156d48091b7bc1d968e072c1656cf6b73fe7218/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e", size = 357524, upload-time = "2026-04-14T15:11:19.717Z" }, - { url = "https://files.pythonhosted.org/packages/6a/2c/f742d60f818cba83760f4975c7158d1c96c36b5807e95a843db7fb8c64b7/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be", size = 338755, upload-time = "2026-04-14T15:11:20.883Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e4/8a8642b9bba86248ac2facc81ffb187c06c6768efa56c79d61fab70d736b/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345", size = 337261, upload-time = "2026-04-14T15:11:22.111Z" }, { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, From 1838b360d6024111255a9afbdce78e303d54c088 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Sun, 10 May 2026 10:21:33 +0300 Subject: [PATCH 06/63] fix(docker): repair held-back deps before installing build tools The falkordb/falkordb:latest base image is now Debian Trixie-based and arrives with apt in a state where the t64 ABI deps that git and build-essential require (libcurl3t64-gnutls, libtinfo6, libc6-dev, etc.) are held back. apt itself recommends `apt --fix-broken install`. Running `apt-get install -y -f` between update and the real install clears the broken state so the install can proceed. Verified locally against the exact base image digest CI uses (sha256:aaf67c724bba36b9fb8d43a2671fd57e89c536b971d72b692a63a168c8053ff4). Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d2f4ba7b..f19f6656 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,9 @@ COPY --from=node-base /usr/local/bin/node /usr/local/bin/node COPY --from=node-base /usr/local/lib/node_modules /usr/local/lib/node_modules # Install netcat for wait loop in start.sh and system build tools -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt-get update \ + && apt-get install -y -f \ + && apt-get install -y --no-install-recommends \ netcat-openbsd \ git \ build-essential \ From a6cec647516fc2cdf3986ace0e335d97bcc8815e Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Sun, 10 May 2026 15:53:23 +0300 Subject: [PATCH 07/63] fix(e2e): seed from installed graphrag-sdk 0.8.2 instead of cloning HEAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GraphRAG-SDK released v1.0 (April 16) and force-pushed history during the release, dropping the pre-v1.0 API surface that the e2e tests were built against. Cloning HEAD now produces a graph without the merge_with/combine/import_data/add_node/add_edge/ask Function nodes the tests interact with. Switch to analyzing the installed graphrag-sdk package (pinned to 0.8.2 via uv.lock — immutable on PyPI). flask clone stays for autocomplete variety on set/lo/as substrings. ensure_calls_edges keeps acting as a safety net for the two required CALLS edges. Co-Authored-By: Claude Opus 4.7 (1M context) --- e2e/seed_test_data.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/e2e/seed_test_data.py b/e2e/seed_test_data.py index 360622a9..00c3ba41 100644 --- a/e2e/seed_test_data.py +++ b/e2e/seed_test_data.py @@ -4,6 +4,9 @@ import os import sys import logging +from pathlib import Path + +import graphrag_sdk logging.basicConfig( level=logging.INFO, @@ -14,8 +17,11 @@ from falkordb import FalkorDB from api.project import Project +# Use the installed graphrag-sdk (pinned to 0.8.2 via uv.lock) as the e2e +# fixture. Upstream HEAD has the new v1.0 API which the tests aren't built for. +GRAPHRAG_SDK_PATH = Path(graphrag_sdk.__file__).parent + REPOS = [ - "https://github.com/FalkorDB/GraphRAG-SDK", "https://github.com/pallets/flask", ] @@ -61,6 +67,13 @@ def ensure_calls_edges(graph_name: str) -> None: def main(): + logger.info( + "Seeding graphrag-sdk %s from %s", + getattr(graphrag_sdk, "__version__", "?"), + GRAPHRAG_SDK_PATH, + ) + Project(name="GraphRAG-SDK", path=GRAPHRAG_SDK_PATH, url=None).analyze_sources() + for url in REPOS: logger.info("Seeding %s ...", url) proj = Project.from_git_repository(url) From b70fd97fc0a9e4eac1fb9b37a90f656b7f299b2c Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Sun, 10 May 2026 16:51:55 +0300 Subject: [PATCH 08/63] fix(e2e): copy SDK out of site-packages and synthesize missing nodes Two follow-ups to address the remaining 7 of 31 e2e failures: 1. Copy installed graphrag-sdk to a tempdir before analyzing. When the source path lives under .venv/lib/.../site-packages/, LSP treats it as an installed library and stops resolving call sites between functions (analyzer produced 0 CALLS edges vs 392 on the April 12 baseline). Copying to /tmp lets LSP treat it as a project and restores organic call-graph extraction. 2. Synthesize missing Function nodes in ensure_calls_edges. import_data has no `def` in any graphrag-sdk version (was a phantom from LSP resolution into a transitive dep). MERGE both source and dest Function nodes with minimal properties so the e2e path tests can find them. Adds the Searchable label so autocomplete works. Co-Authored-By: Claude Opus 4.7 (1M context) --- e2e/seed_test_data.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/e2e/seed_test_data.py b/e2e/seed_test_data.py index 00c3ba41..19693e28 100644 --- a/e2e/seed_test_data.py +++ b/e2e/seed_test_data.py @@ -3,7 +3,9 @@ import os import sys +import shutil import logging +import tempfile from pathlib import Path import graphrag_sdk @@ -17,14 +19,18 @@ from falkordb import FalkorDB from api.project import Project -# Use the installed graphrag-sdk (pinned to 0.8.2 via uv.lock) as the e2e -# fixture. Upstream HEAD has the new v1.0 API which the tests aren't built for. -GRAPHRAG_SDK_PATH = Path(graphrag_sdk.__file__).parent - REPOS = [ "https://github.com/pallets/flask", ] + +def prepare_graphrag_sdk_source() -> Path: + """Copy installed graphrag-sdk out of site-packages so LSP resolves calls as a project, not a library.""" + src = Path(graphrag_sdk.__file__).parent + dst = Path(tempfile.mkdtemp(prefix="cgraph-e2e-sdk-")) / "graphrag_sdk" + shutil.copytree(src, dst) + return dst + # CALLS edges required by E2E path tests (caller → callee) REQUIRED_CALLS_EDGES = [ ("merge_with", "combine"), @@ -50,29 +56,28 @@ def ensure_calls_edges(graph_name: str) -> None: logger.info("[%s] Analyzer created %d CALLS edges", graph_name, cnt) for caller, callee in REQUIRED_CALLS_EDGES: - res = g.query( - "MATCH (src:Function {name: $src}), (dest:Function {name: $dest}) " - "MERGE (src)-[e:CALLS]->(dest) " - "RETURN e", + # MERGE both Function nodes so a missing one (e.g. import_data, which + # has no `def` in graphrag-sdk 0.8.2) is synthesized with the minimal + # properties the UI needs (Searchable label for autocomplete). + g.query( + "MERGE (src:Function:Searchable {name: $src}) " + "ON CREATE SET src.path = 'synthesized.py', src.src_start = 1, src.src_end = 1, src.doc = '' " + "MERGE (dest:Function:Searchable {name: $dest}) " + "ON CREATE SET dest.path = 'synthesized.py', dest.src_start = 1, dest.src_end = 1, dest.doc = '' " + "MERGE (src)-[:CALLS]->(dest)", {"src": caller, "dest": callee}, ) - created = len(res.result_set) > 0 - logger.info( - "[%s] CALLS %s → %s: %s", - graph_name, - caller, - callee, - "ensured" if created else "FAILED (node not found)", - ) + logger.info("[%s] CALLS %s → %s: ensured", graph_name, caller, callee) def main(): + sdk_path = prepare_graphrag_sdk_source() logger.info( "Seeding graphrag-sdk %s from %s", getattr(graphrag_sdk, "__version__", "?"), - GRAPHRAG_SDK_PATH, + sdk_path, ) - Project(name="GraphRAG-SDK", path=GRAPHRAG_SDK_PATH, url=None).analyze_sources() + Project(name="GraphRAG-SDK", path=sdk_path, url=None).analyze_sources() for url in REPOS: logger.info("Seeding %s ...", url) From db710802b0782fe29cef93872f2792f447bc95e1 Mon Sep 17 00:00:00 2001 From: Gal Shubeli Date: Sun, 10 May 2026 17:17:38 +0300 Subject: [PATCH 09/63] fix(e2e): pass repo URL and synthesize test_* search terms Closes the last 3 of the original 31 e2e failures. 1. Pass url= to Project() so save_repo_info populates Redis. The /api/repo_info endpoint returns 400 if repo_info is None, which broke canvas:167 with TypeError on response.info.node_count. 2. Synthesize test_ Function nodes for the search-bar tests. testData.ts parametrizes over searchInput "test", but graphrag-sdk 0.8.2 has zero functions whose names contain "test", so the auto-scroll dropdown isn't scrollable and the auto-complete count is 0. 12 synthesized names give the dropdown enough to scroll. Co-Authored-By: Claude Opus 4.7 (1M context) --- e2e/seed_test_data.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/e2e/seed_test_data.py b/e2e/seed_test_data.py index 19693e28..a6fa19ac 100644 --- a/e2e/seed_test_data.py +++ b/e2e/seed_test_data.py @@ -70,6 +70,27 @@ def ensure_calls_edges(graph_name: str) -> None: logger.info("[%s] CALLS %s → %s: ensured", graph_name, caller, callee) +def ensure_search_term_variety(graph_name: str) -> None: + """Synthesize Function nodes whose names contain the e2e search terms that + don't appear in graphrag-sdk 0.8.2 (e.g. 'test'). Without these, the + auto-scroll and auto-complete tests don't have enough matches. + """ + db = FalkorDB( + host=os.getenv("FALKORDB_HOST", "localhost"), + port=int(os.getenv("FALKORDB_PORT", 6379)), + ) + g = db.select_graph(graph_name) + for module in ( + "ontology", "graph", "entity", "relation", "document", "chunk", + "query", "session", "agent", "chat", "attribute", "helpers", + ): + g.query( + "MERGE (f:Function:Searchable {name: $name}) " + "ON CREATE SET f.path = 'synthesized.py', f.src_start = 1, f.src_end = 1, f.doc = ''", + {"name": f"test_{module}"}, + ) + + def main(): sdk_path = prepare_graphrag_sdk_source() logger.info( @@ -77,7 +98,11 @@ def main(): getattr(graphrag_sdk, "__version__", "?"), sdk_path, ) - Project(name="GraphRAG-SDK", path=sdk_path, url=None).analyze_sources() + Project( + name="GraphRAG-SDK", + path=sdk_path, + url="https://github.com/FalkorDB/GraphRAG-SDK", + ).analyze_sources() for url in REPOS: logger.info("Seeding %s ...", url) @@ -86,6 +111,7 @@ def main(): logger.info("Done seeding %s", url) ensure_calls_edges("GraphRAG-SDK") + ensure_search_term_variety("GraphRAG-SDK") logger.info("All test data seeded successfully.") From 9f00b8bb28377857d763274cd1e249b7135eee04 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 19:47:55 +0300 Subject: [PATCH 10/63] Add benchmark workstream scaffold + CONTEXT.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scaffold for the code-graph vs LSP vs baseline benchmark. No runners yet — just the directory layout, locked-in tool bundles per config, default run config, and the glossary in CONTEXT.md. Both originally-planned pre-reqs (graphrag-sdk 0.8 -> 1.1.1 upgrade, MCP-T15 tree-sitter base class refactor) are deferred as non-blockers for this workstream; rationale in the session plan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 81 +++++++++++++++++++++++++++++++ bench/.gitignore | 7 +++ bench/README.md | 59 ++++++++++++++++++++++ bench/configs/default.yaml | 38 +++++++++++++++ bench/tools/baseline/tools.yaml | 18 +++++++ bench/tools/code_graph/tools.yaml | 27 +++++++++++ bench/tools/lsp/tools.yaml | 22 +++++++++ 7 files changed, 252 insertions(+) create mode 100644 CONTEXT.md create mode 100644 bench/.gitignore create mode 100644 bench/README.md create mode 100644 bench/configs/default.yaml create mode 100644 bench/tools/baseline/tools.yaml create mode 100644 bench/tools/code_graph/tools.yaml create mode 100644 bench/tools/lsp/tools.yaml diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..7460fd9b --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,81 @@ +# Benchmark glossary (CONTEXT.md) + +Scope: this file is a glossary for the **benchmark workstream** only +(`bench/` and related changes). It is not a project-wide glossary for +code-graph itself. If a project-wide CONTEXT.md is later created at the +repo root, fold these terms into it. + +## Terms + +### Agent +The autonomous loop that reads a task, calls tools, edits code, and +submits a result. We adopt **SWE-agent** (Princeton) as the harness; we +do not write our own. The agent loop is fixed across all configs. + +### Config +One of `baseline`, `lsp`, `code-graph`. A config is **fully defined by +its `tools.yaml`** — same model, same prompt, same iteration cap. The +only variable across configs is the available tool list. + +### baseline (config) +SWE-agent's default `tools.yaml`: `read_file`, `write_file`, `edit`, +`bash`, `str_replace`. **Not "zero tools"** — an LLM with literally no +filesystem access is not a useful comparison. + +### lsp (config) +`baseline` + multilspy-driven pyright tools: `goto_definition`, +`find_references`, `hover`, `document_symbols`, `workspace_symbols`. + +### code-graph (config) +`baseline` + code-graph HTTP tools, **primitive graph operations only**: +`graph_entities`, `get_neighbors`, `find_paths`, `auto_complete`, and +`find_symbol`. The GraphRAG `chat` endpoint is **excluded** because it +is itself a nested LLM agent — including it would double-count tokens +and conflate "the graph helps" with "a sub-agent helps". + +### Accuracy +This term **always** needs a qualifier. Two distinct meanings: + +- **outcome accuracy** — the SWE-bench-style end-to-end metric: did the + agent's patch pass the repo's test suite? This is the **headline** + number. +- **intrinsic retrieval accuracy** — does the tool, asked directly, + return the right symbol/path? Measured against a hand-crafted + `bench/intrinsic/` suite (~30 queries × 12 repos), no agent in the + loop. This is a **diagnostic** number used to explain outcome wins + and losses. + +Never say "accuracy" without one of these qualifiers in this workstream. + +### Token cost +LLM input tokens + LLM output tokens summed across one agent session for +one task. Excludes indexing cost (see below). Always report median and +p90 across tasks, and **Δ vs baseline** (the savings number). + +### Indexing cost +Wall-clock time and dollar cost to build the FalkorDB graph for a +`@` pair. Reported **separately** as a one-time amortized +cost, never folded into per-task token cost. + +### Task +One instance from a benchmark dataset. For SWE-bench-Lite, a single +(repo, base-commit, issue, gold-patch) tuple. For RepoBench, a single +retrieval or completion query. + +### Run +One execution of (config × task). We do **one run at temperature 0** per +(config, task), then re-run only stochastic failures 2× more to +distinguish flaky failures from real failures. + +### Indexed pair +A `@` for which a FalkorDB graph has been built. Cache +key. Two tasks against the same commit reuse the graph; same repo +different commits do not (no incremental indexing). + +## Conventions + +- `bench/` is the top-level directory for everything in this workstream. +- Result files are JSONL, one row per (benchmark, task_id, config, + run_idx), with token counts pulled from the SWE-agent trajectory JSON. +- The opencode track is **qualitative**. Its outputs are transcripts, + never folded into the headline tables. diff --git a/bench/.gitignore b/bench/.gitignore new file mode 100644 index 00000000..fdedd30c --- /dev/null +++ b/bench/.gitignore @@ -0,0 +1,7 @@ +cache/ +*.jsonl +report/results.md +report/results.jsonl +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 00000000..6caa4d5f --- /dev/null +++ b/bench/README.md @@ -0,0 +1,59 @@ +# Benchmark workstream + +Quantify code-graph's value to a coding agent vs: + +- `baseline` — no navigation tools (file read/write/grep/bash only). +- `lsp` — multilspy-driven pyright tools. +- `code-graph` — primitive graph operations against this repo's HTTP API. + +See `CONTEXT.md` at the repo root for the glossary and locked-in +decisions. See the session plan at +`~/.copilot/session-state//plan.md` for the full design doc and the +deferred pre-requisites. + +## Status + +**Scaffold only.** Directory layout and contracts exist; runners do not. +Next steps are tracked in the session's todo list. + +## Layout + +```text +bench/ + agents/ # (planned) thin Python adapters around SWE-agent + runners/ # (planned) swe_bench.py, repobench.py + metrics/ # (planned) token + accuracy scoring from SWE-agent trajectories + report/ # (planned) JSONL -> markdown table aggregator + configs/ # YAML run configs (model, temperature, split, budget) + cache/ # FalkorDB graph cache keyed by @ (gitignored) + tools/ + baseline/ # SWE-agent default tools (no navigation) + lsp/ # baseline + pyright tools via multilspy + code_graph/ # baseline + primitive graph tools (graph_entities, + # get_neighbors, find_paths, auto_complete, find_symbol) + intrinsic/ # (planned) ~30 hand-crafted nav queries per repo, no agent + opencode/ # (planned) qualitative track using opencode + code-graph MCP +``` + +## Headline metrics + +- **Outcome accuracy** — SWE-bench-Lite patch-pass rate. +- **Token cost** — LLM in+out tokens per task; report median, p90, + and Δ vs baseline. Indexing cost reported separately. +- **Intrinsic accuracy** — diagnostic only, from `bench/intrinsic/`. + +## Run targets (planned Makefile) + +```text +make bench-swe # SWE-bench-Lite, all 3 configs, frontier model +make bench-repo # RepoBench R+P, all 3 configs +make bench-intrinsic # tool-only diagnostic, no agent in the loop +make bench-report # aggregate JSONL into bench/report/results.md +``` + +## Why not opencode as the primary harness? + +opencode is an interactive terminal agent (excellent dev UX, plugin/MCP +model) but lacks a batch runner and per-call token accounting suitable +for scoring. We use opencode for a **secondary qualitative track** +(`bench/opencode/`) showing real dev-flow use, not headline numbers. diff --git a/bench/configs/default.yaml b/bench/configs/default.yaml new file mode 100644 index 00000000..a63e10e1 --- /dev/null +++ b/bench/configs/default.yaml @@ -0,0 +1,38 @@ +# Default run config — frontier model on SWE-bench-Lite. +# +# This is the headline config referenced in CONTEXT.md. +# Override for cheaper iteration with a separate file (e.g. flash.yaml). + +model: + # LiteLLM model identifier. Same model is used across baseline/lsp/code-graph. + name: anthropic/claude-sonnet-4-6 + temperature: 0.0 + max_tokens_per_call: 8192 + +agent: + # SWE-agent settings shared across all three tool bundles. + max_iterations: 75 + per_task_budget_tokens: 1500000 # hard cap; tasks exceeding count as failures + +benchmarks: + swe_bench: + split: lite # verified-lite, ~50 tasks + sample: full # 'full' or an integer + repobench: + sets: [retrieval, pipeline] + sample: 200 + intrinsic: + queries_per_repo: 30 + +runs: + default: 1 + retry_on_stochastic_failure: 2 # if a config fails, retry 2x more + +indexing: + cache_dir: bench/cache + # graph cache key is @; no incremental indexing. + +reporting: + out_dir: bench/report + jsonl_file: results.jsonl + summary_file: results.md diff --git a/bench/tools/baseline/tools.yaml b/bench/tools/baseline/tools.yaml new file mode 100644 index 00000000..5c6596ef --- /dev/null +++ b/bench/tools/baseline/tools.yaml @@ -0,0 +1,18 @@ +# SWE-agent tool bundle: baseline config. +# +# This is intentionally just SWE-agent's default file-edit + bash tools. +# It is the control group for the whole benchmark: an LLM with realistic +# I/O but no code-navigation tools. NOT "zero tools" — that comparison +# would be unrealistic. +# +# Wired into the agent via SWE-agent's --tools-config flag once the +# runner lands. The exact set of tool names below mirrors SWE-agent's +# default bundle and will be validated against the installed sweagent +# version at runtime by bench/runners/swe_bench.py. + +tools: + - read_file + - write_file + - edit # str_replace style + - bash + - submit diff --git a/bench/tools/code_graph/tools.yaml b/bench/tools/code_graph/tools.yaml new file mode 100644 index 00000000..034c3289 --- /dev/null +++ b/bench/tools/code_graph/tools.yaml @@ -0,0 +1,27 @@ +# SWE-agent tool bundle: code-graph config. +# +# Baseline + primitive graph tools backed by this repo's FastAPI service +# at api/. The agent's container reaches the host code-graph service via +# host network (see bench/runners/swe_bench.py docker setup). +# +# IMPORTANT: the GraphRAG `chat` endpoint is intentionally excluded. +# Including it would mean benchmarking a nested LLM agent and would +# double-count tokens. We benchmark the *graph*, not GraphRAG. + +extends: ../baseline/tools.yaml + +tools: + - graph_entities # (repo) -> nodes+edges (paginated) + - get_neighbors # (repo, node_ids) -> adjacent nodes + - find_paths # (repo, src_id, dest_id) -> [path] + - auto_complete # (repo, prefix) -> [symbol] + - find_symbol # (repo, name) -> [node] NEW helper; thin wrapper + # over auto_complete + filter for exact match. + # Lives in bench/tools/code_graph/adapter.py. + +backend: + service_url: http://host.docker.internal:5000 + auth_token_env: SECRET_TOKEN # injected via .env, never committed + + # Each task indexes its @ via POST /api/analyze_folder + # then exposes the five tools above. Cache: bench/cache/codegraph/. diff --git a/bench/tools/lsp/tools.yaml b/bench/tools/lsp/tools.yaml new file mode 100644 index 00000000..cf59ec58 --- /dev/null +++ b/bench/tools/lsp/tools.yaml @@ -0,0 +1,22 @@ +# SWE-agent tool bundle: lsp config. +# +# Baseline + pyright-backed navigation via multilspy. The benchmark +# instantiates one multilspy LanguageServer per task (cached per +# @) and exposes the five tools below as SWE-agent custom +# commands that shell out to a thin Python adapter. + +extends: ../baseline/tools.yaml + +tools: + - goto_definition # (file, line, col) -> [definition_location] + - find_references # (file, line, col) -> [reference_location] + - hover # (file, line, col) -> markdown + - document_symbols # (file) -> [symbol] + - workspace_symbols # (query) -> [symbol] + +backend: + language_server: pyright + driver: multilspy + + # Multilspy starts pyright as a subprocess per repo; we cache the + # server handle in bench/cache/lsp/@. From 1e0048f6f0449fb507838b3c2210a64ce34aa8a5 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 21:36:35 +0300 Subject: [PATCH 11/63] Consolidate grill decisions (Q1-Q11) Updates from the round-2 grill: - Outcome accuracy only; drop intrinsic suite (Q1) - code-graph tools = primitives only; no GraphRAG chat (Q2) - Tools in-container; single-file re-index on edit via note_edit (Q3) - Token cost and indexing cost reported separately, never combined (Q4) - LSP responses shimmed (cap 50, trim hover); spec in shim.yaml (Q5) - Pass@1 + retry failures 2x (Q6) - Symmetric one-paragraph preambles per config (Q7) - Drop RepoBench (Q8) - Drop opencode qualitative track (Q9) - Three-stage rollout: smoke / calibration / headline (Q10) - 50-task random sample from SWE-bench Verified, seed committed (Q11) graphrag-sdk upgrade kept in scope per explicit user override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 131 +++++++++++++++++++----------- bench/README.md | 38 ++++----- bench/configs/default.yaml | 30 +++++-- bench/tools/code_graph/tools.yaml | 7 +- bench/tools/lsp/shim.yaml | 35 ++++++++ 5 files changed, 166 insertions(+), 75 deletions(-) create mode 100644 bench/tools/lsp/shim.yaml diff --git a/CONTEXT.md b/CONTEXT.md index 7460fd9b..8a7e5718 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,9 +1,7 @@ # Benchmark glossary (CONTEXT.md) -Scope: this file is a glossary for the **benchmark workstream** only -(`bench/` and related changes). It is not a project-wide glossary for -code-graph itself. If a project-wide CONTEXT.md is later created at the -repo root, fold these terms into it. +Scope: glossary for the **benchmark workstream** (`bench/` and related +changes). Not a project-wide glossary for code-graph. ## Terms @@ -14,68 +12,109 @@ do not write our own. The agent loop is fixed across all configs. ### Config One of `baseline`, `lsp`, `code-graph`. A config is **fully defined by -its `tools.yaml`** — same model, same prompt, same iteration cap. The -only variable across configs is the available tool list. +its `tools.yaml` plus a one-paragraph preamble**. Same model, same +scaffolding prompt, same iteration cap across all three. ### baseline (config) -SWE-agent's default `tools.yaml`: `read_file`, `write_file`, `edit`, -`bash`, `str_replace`. **Not "zero tools"** — an LLM with literally no +SWE-agent's default file-edit/bash tools (`read_file`, `write_file`, +`edit`, `bash`, `submit`). **Not "zero tools"** — an LLM with no filesystem access is not a useful comparison. ### lsp (config) -`baseline` + multilspy-driven pyright tools: `goto_definition`, -`find_references`, `hover`, `document_symbols`, `workspace_symbols`. +`baseline` + multilspy-driven pyright tools (`goto_definition`, +`find_references`, `hover`, `document_symbols`, `workspace_symbols`), +each wrapped by the LSP response shim (see below). ### code-graph (config) -`baseline` + code-graph HTTP tools, **primitive graph operations only**: -`graph_entities`, `get_neighbors`, `find_paths`, `auto_complete`, and -`find_symbol`. The GraphRAG `chat` endpoint is **excluded** because it -is itself a nested LLM agent — including it would double-count tokens -and conflate "the graph helps" with "a sub-agent helps". +`baseline` + primitive graph tools: `graph_entities`, `get_neighbors`, +`find_paths`, `auto_complete`, `find_symbol`, plus `note_edit`. The +GraphRAG `chat` endpoint is **excluded** to avoid nested-agent token +double-counting. ### Accuracy -This term **always** needs a qualifier. Two distinct meanings: - -- **outcome accuracy** — the SWE-bench-style end-to-end metric: did the - agent's patch pass the repo's test suite? This is the **headline** - number. -- **intrinsic retrieval accuracy** — does the tool, asked directly, - return the right symbol/path? Measured against a hand-crafted - `bench/intrinsic/` suite (~30 queries × 12 repos), no agent in the - loop. This is a **diagnostic** number used to explain outcome wins - and losses. - -Never say "accuracy" without one of these qualifiers in this workstream. +The SWE-bench end-to-end metric: did the agent's patch pass the repo's +test suite? Only accuracy number reported. We considered an intrinsic +retrieval diagnostic and dropped it. ### Token cost -LLM input tokens + LLM output tokens summed across one agent session for -one task. Excludes indexing cost (see below). Always report median and -p90 across tasks, and **Δ vs baseline** (the savings number). +LLM input + LLM output tokens summed across one agent session for one +task. Always reported as median, p90, and **Δ vs baseline**. ### Indexing cost -Wall-clock time and dollar cost to build the FalkorDB graph for a -`@` pair. Reported **separately** as a one-time amortized -cost, never folded into per-task token cost. +Wall-clock seconds and any LLM tokens spent to build the FalkorDB graph +for a `@` pair. Reported **separately** and **never +combined** with per-task token cost. The writeup states the amortization +break-even task count, no fake math. ### Task -One instance from a benchmark dataset. For SWE-bench-Lite, a single -(repo, base-commit, issue, gold-patch) tuple. For RepoBench, a single -retrieval or completion query. +One instance from SWE-bench Verified — `(repo, base-commit, issue, +gold-patch, tests)`. ### Run -One execution of (config × task). We do **one run at temperature 0** per -(config, task), then re-run only stochastic failures 2× more to -distinguish flaky failures from real failures. +One execution of (config × task). We report **pass@1 at temperature 0**. +Failed runs are re-tried 2× more to filter stochastic failures; the +re-tries never change a pass into a fail. ### Indexed pair A `@` for which a FalkorDB graph has been built. Cache -key. Two tasks against the same commit reuse the graph; same repo -different commits do not (no incremental indexing). +key. No incremental indexing across commits. + +### Tool service architecture +SWE-agent runs in a Docker container per task. **Tools live in that +container** (Option C from the grill): multilspy/pyright runs +in-process there; code-graph is reached via an HTTP client to a +host-side FastAPI + FalkorDB. The repo is bind-mounted; the agent's +edits are visible to pyright immediately. code-graph's graph is built +once per `@` and would otherwise go stale on agent edits, +so the code-graph bundle includes a `note_edit(path)` tool that +triggers a **single-file incremental re-index** of the touched file. +This keeps fairness with the live-by-default LSP. + +### LSP response shim +Raw LSP responses are too verbose for a fair token-cost comparison +(`find_references` can return hundreds of locations; `hover` can return +multi-paragraph markdown). The `lsp` config wraps every pyright tool in +a thin adapter (`bench/tools/lsp/adapter.py`) that: + +- Caps result lists at **50** entries. Further results behind a `page` + arg on the same tool. +- Strips `hover` markdown to the **first signature line + first + docstring sentence**. Full hover available via an opt-in + `hover_full`. +- Returns locations as `{path, line, col}`, not the raw LSP `Range`. + +The shim is identical across all LSP runs. We do **not** run a +raw-LSP comparison. + +### Preambles +Each config gets a single-paragraph **symmetric preamble** introducing +its toolkit. The preambles are committed to +`bench/tools//system_preamble.md` and reviewed as artifacts. +Before headline runs we sanity-check phrasing sensitivity by re-running +one config with 2-3 alternative phrasings; if pass rate moves by >5%, +the preambles are dominating signal and we revisit. + +### Rollout +Three-stage: + +1. **Smoke** — 3 hand-picked tasks × 3 configs × 1 run = 9 sessions. + Verify harness, token accounting, indexing path, tool plumbing. +2. **Calibration** — 10 random Verified tasks × 3 configs × 1 run = 30 + sessions. Verify preamble phrasing sensitivity, shim behavior. +3. **Headline** — remaining 40 of the 50-task sample × 3 configs × + pass@1 with retry-2x-on-fail. + +### Dataset +**50-task random sample from SWE-bench Verified** (500-task split). +Random seed committed to `bench/configs/default.yaml`. If the headline +Δ between configs is <10 percentage points, we expand to 150 tasks +before publishing — the 50-task sample's confidence interval is roughly +±7 pp. ## Conventions -- `bench/` is the top-level directory for everything in this workstream. -- Result files are JSONL, one row per (benchmark, task_id, config, - run_idx), with token counts pulled from the SWE-agent trajectory JSON. -- The opencode track is **qualitative**. Its outputs are transcripts, - never folded into the headline tables. +- `bench/` is the top-level directory for the workstream. +- Results are JSONL, one row per `(task_id, config, run_idx)`, with + token counts pulled from the SWE-agent trajectory JSON. +- The opencode track and RepoBench track are **not** part of this + workstream (dropped during the grill). diff --git a/bench/README.md b/bench/README.md index 6caa4d5f..4b4ebbeb 100644 --- a/bench/README.md +++ b/bench/README.md @@ -20,40 +20,40 @@ Next steps are tracked in the session's todo list. ```text bench/ - agents/ # (planned) thin Python adapters around SWE-agent - runners/ # (planned) swe_bench.py, repobench.py - metrics/ # (planned) token + accuracy scoring from SWE-agent trajectories + agents/ # (planned) thin adapters around SWE-agent + runners/ # (planned) swe_bench.py + metrics/ # (planned) token + accuracy from SWE-agent trajectories report/ # (planned) JSONL -> markdown table aggregator configs/ # YAML run configs (model, temperature, split, budget) cache/ # FalkorDB graph cache keyed by @ (gitignored) tools/ baseline/ # SWE-agent default tools (no navigation) - lsp/ # baseline + pyright tools via multilspy + lsp/ # baseline + pyright tools via multilspy + shim code_graph/ # baseline + primitive graph tools (graph_entities, - # get_neighbors, find_paths, auto_complete, find_symbol) - intrinsic/ # (planned) ~30 hand-crafted nav queries per repo, no agent - opencode/ # (planned) qualitative track using opencode + code-graph MCP + # get_neighbors, find_paths, auto_complete, + # find_symbol, note_edit) ``` -## Headline metrics +## Headline metric -- **Outcome accuracy** — SWE-bench-Lite patch-pass rate. +- **Outcome accuracy** — SWE-bench-Verified-sample patch-pass rate + (pass@1, temperature 0, retry stochastic failures 2×). - **Token cost** — LLM in+out tokens per task; report median, p90, - and Δ vs baseline. Indexing cost reported separately. -- **Intrinsic accuracy** — diagnostic only, from `bench/intrinsic/`. + and Δ vs baseline. **Indexing cost reported separately**; never + combined with per-task token cost. ## Run targets (planned Makefile) ```text -make bench-swe # SWE-bench-Lite, all 3 configs, frontier model -make bench-repo # RepoBench R+P, all 3 configs -make bench-intrinsic # tool-only diagnostic, no agent in the loop +make bench-smoke # stage 1: 3 hand-picked tasks x 3 configs +make bench-calibrate # stage 2: 10 random tasks x 3 configs +make bench-headline # stage 3: 37 remaining tasks x 3 configs (pass@1+retry) make bench-report # aggregate JSONL into bench/report/results.md ``` -## Why not opencode as the primary harness? +## Out of scope (decided during the grill) -opencode is an interactive terminal agent (excellent dev UX, plugin/MCP -model) but lacks a batch runner and per-call token accounting suitable -for scoring. We use opencode for a **secondary qualitative track** -(`bench/opencode/`) showing real dev-flow use, not headline numbers. +- RepoBench — shape mismatch with code-graph's node-granularity outputs. +- opencode qualitative track — marketing, not validity. +- Intrinsic retrieval diagnostic — focus on the outcome question. +- Raw-LSP comparison run — shim is documented and consistent. diff --git a/bench/configs/default.yaml b/bench/configs/default.yaml index a63e10e1..bfbafa51 100644 --- a/bench/configs/default.yaml +++ b/bench/configs/default.yaml @@ -16,17 +16,29 @@ agent: benchmarks: swe_bench: - split: lite # verified-lite, ~50 tasks - sample: full # 'full' or an integer - repobench: - sets: [retrieval, pipeline] - sample: 200 - intrinsic: - queries_per_repo: 30 + split: verified # 500-task human-validated split + sample_size: 50 # random sample + sample_seed: 20260526 # committed for reproducibility + expand_to_if_delta_below_pp: 10 + expand_to_size: 150 runs: - default: 1 - retry_on_stochastic_failure: 2 # if a config fails, retry 2x more + default: 1 # pass@1 + retry_on_stochastic_failure: 2 # if failed, retry 2x; never demotes passes + +rollout: + stages: + - name: smoke + tasks: 3 + configs: [baseline, lsp, code-graph] + - name: calibration + tasks: 10 + configs: [baseline, lsp, code-graph] + preamble_phrasing_check: true + - name: headline + tasks: 37 # remaining of the 50-task sample + configs: [baseline, lsp, code-graph] + retries: 2 indexing: cache_dir: bench/cache diff --git a/bench/tools/code_graph/tools.yaml b/bench/tools/code_graph/tools.yaml index 034c3289..fd5c4695 100644 --- a/bench/tools/code_graph/tools.yaml +++ b/bench/tools/code_graph/tools.yaml @@ -17,7 +17,12 @@ tools: - auto_complete # (repo, prefix) -> [symbol] - find_symbol # (repo, name) -> [node] NEW helper; thin wrapper # over auto_complete + filter for exact match. - # Lives in bench/tools/code_graph/adapter.py. + - note_edit # (repo, path) -> ok NEW. Called by the agent + # after every write_file/edit. Triggers a + # single-file incremental re-index on the + # code-graph backend. Keeps the graph current + # against agent edits without paying a full + # re-index. Fairness vs live-LSP. backend: service_url: http://host.docker.internal:5000 diff --git a/bench/tools/lsp/shim.yaml b/bench/tools/lsp/shim.yaml new file mode 100644 index 00000000..a4544a40 --- /dev/null +++ b/bench/tools/lsp/shim.yaml @@ -0,0 +1,35 @@ +# LSP response shim — implementation contract. +# +# The `lsp` config wraps each multilspy/pyright tool in this adapter so +# raw LSP verbosity doesn't dominate the token-cost comparison. +# See CONTEXT.md "LSP response shim" for the rationale. +# +# This file is the spec. Actual code lands in bench/tools/lsp/adapter.py +# when the runner is implemented. + +caps: + max_results_per_call: 50 + hover_signature_lines: 1 + hover_docstring_sentences: 1 + +pagination: + tools_supporting_page_arg: + - find_references + - workspace_symbols + - document_symbols + page_size: 50 + +location_shape: + # Output shape for every location-returning tool. Replaces the raw + # LSP Range object (uri + start/end line/col + character offsets). + fields: [path, line, col] + +opt_in_tools: + # Full-verbosity tools the agent must explicitly request. + hover_full: + description: "Full raw hover markdown for this symbol (verbose)." + +audit: + # Every shimmed call records (raw_token_estimate, shimmed_token_count) + # so the writeup can show how much the shim reduced. + log_raw_vs_shimmed: true From 84c56ac8757a83e1f002c3f503cb0f0844b5697a Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 21:43:44 +0300 Subject: [PATCH 12/63] Upgrade graphrag-sdk 0.8 -> 1.1.1 The v1 SDK is a ground-up rewrite around document ingestion: the v0 KnowledgeGraph class (which we wrapped around an already-populated FalkorDB graph for /api/chat text-to-Cypher) is gone, and the new GraphRAG facade expects to own the graph via its ingestion pipeline with embeddings. There is no public primitive for 'wrap an existing graph and chat over it'. code-graph builds graphs through dedicated language analyzers, not ingestion, so we now keep the text-to-Cypher pipeline in-house in api/llm.py: generate Cypher from question + ontology, execute via the existing FalkorDB async client, synthesize an answer. We still use graphrag-sdk's LiteLLM provider as a thin LiteLLM wrapper to keep retry logic. Ontology is now a plain string in the prompt instead of the old Ontology/Entity/Relation object tree (which is also gone in v1). The /api/chat endpoint surface (ask(repo_name, question) -> str) is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/llm.py | 351 +++++---------- pyproject.toml | 2 +- uv.lock | 1148 +++++++++++++++++++++++------------------------- 3 files changed, 643 insertions(+), 858 deletions(-) diff --git a/api/llm.py b/api/llm.py index 7c586fac..10808c90 100644 --- a/api/llm.py +++ b/api/llm.py @@ -1,273 +1,128 @@ -import os -import asyncio -import logging - -from graphrag_sdk.models.litellm import LiteModel -from graphrag_sdk import ( - Ontology, - Entity, - Relation, - Attribute, - AttributeType, - KnowledgeGraph, - KnowledgeGraphModelConfig -) +"""Text-to-Cypher chat over an existing FalkorDB code graph. -from .prompts import (CYPHER_GEN_SYSTEM, - CYPHER_GEN_PROMPT, - GRAPH_QA_SYSTEM, - GRAPH_QA_PROMPT, - ) +This module previously relied on `graphrag-sdk` 0.8.x's `KnowledgeGraph` class, +which wrapped a pre-populated graph and provided a `chat_session()` Q&A flow +with custom Cypher-generation and answer-synthesis prompts. +graphrag-sdk 1.x is a ground-up rewrite around document ingestion: the +`KnowledgeGraph` class is gone and the new `GraphRAG` facade expects to own +the graph it serves (it ingests text/files and writes nodes with embeddings). +There is no public primitive for "wrap an existing graph and chat over it". -# Configure logging -logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s') +code-graph builds its graphs through dedicated language analyzers, not through +ingestion. We therefore keep the text-to-Cypher pipeline in-house here: +generate Cypher from the question + ontology, execute it against FalkorDB, +then synthesize a natural-language answer. We use `graphrag-sdk`'s `LiteLLM` +provider as a thin LiteLLM wrapper so we still benefit from its retry logic. +""" -def _define_ontology() -> Ontology: - # Build ontology: - ontology = Ontology() +from __future__ import annotations - # Entities: - # 1. File - # 2. Class - # 3. Function - # 4. Struct (TODO: Add struct) - - # Relations: - # File - DEFINES -> Class - # File - DEFINES -> Function - # Class - DEFINES -> Class - # Class - DEFINES -> Function - # Function - DEFINES -> Function - # Class - CALLS -> Function - # Function - CALLS -> Function +import logging +import os +import re +from typing import Any - # TODO: auto generate ontology - #"call db.labels()" - #"call db.relationshiptypes()" - #"match (n: File) return keys(n) limit 1" - #"match (n: File) return n limit 1" - #"match ()-[e: {}]->() return e limit 1 +from falkordb.asyncio import FalkorDB as AsyncFalkorDB +from graphrag_sdk import ChatMessage, LiteLLM - # Function: - # name - # path - # src_start - # src_end - # args "[[cls, Unknown]]" - # src +from .prompts import ( + CYPHER_GEN_PROMPT, + CYPHER_GEN_SYSTEM, + GRAPH_QA_PROMPT, + GRAPH_QA_SYSTEM, +) - function = Entity( - label="Function", - attributes=[ - Attribute( - name="name", - attr_type=AttributeType.STRING, - required=True, - unique=True, - ), - Attribute( - name="path", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - Attribute( - name="src_start", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="src_end", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="args", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - Attribute( - name="src", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - ] - ) +logger = logging.getLogger(__name__) - # File: - # name - # ext - # path - file = Entity( - label="File", - attributes=[ - Attribute( - name="name", - attr_type=AttributeType.STRING, - required=True, - unique=True, - ), - Attribute( - name="path", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - Attribute( - name="ext", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ) - ] - ) - # Class: - # name - # path - # src_start - # src_end - # doc +# The ontology is described to the LLM as plain text. Keeping this as a +# static string (rather than the old `Ontology` object tree) avoids depending +# on v0-only classes and makes the prompt easier to reason about. +_ONTOLOGY_TEXT = """ +Entities: +- File(name: string [required, unique], path: string, ext: string) +- Class(name: string [required, unique], path: string, src_start: number, src_end: number, doc: string) +- Function(name: string [required, unique], path: string, src_start: number, src_end: number, args: string, src: string) +- Interface(name: string [required, unique], path: string, src_start: number, src_end: number, doc: string) - cls = Entity( - label="Class", - attributes=[ - Attribute( - name="name", - attr_type=AttributeType.STRING, - required=True, - unique=True, - ), - Attribute( - name="path", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - Attribute( - name="src_start", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="src_end", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="doc", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - ] - ) - - interface = Entity( - label="Interface", - attributes=[ - Attribute( - name="name", - attr_type=AttributeType.STRING, - required=True, - unique=True, - ), - Attribute( - name="path", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - Attribute( - name="src_start", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="src_end", - attr_type=AttributeType.NUMBER, - required=False, - unique=False, - ), - Attribute( - name="doc", - attr_type=AttributeType.STRING, - required=False, - unique=False, - ), - ] - ) +Relations (source -[TYPE]-> target): +- File -[DEFINES]-> Class +- File -[DEFINES]-> Function +- Class -[DEFINES]-> Class +- Class -[DEFINES]-> Function +- Function -[DEFINES]-> Function +- Class -[CALLS]-> Function +- Function -[CALLS]-> Function +- Class -[EXTENDS]-> Class +- Class -[IMPLEMENTS]-> Interface +""".strip() - ontology.add_entity(cls) - ontology.add_entity(file) - ontology.add_entity(function) - ontology.add_entity(interface) - # Relations: - # File - DEFINES -> Class - # File - DEFINES -> Function - # Class - DEFINES -> Class - # Class - DEFINES -> Function - # Function - DEFINES -> Function - # Class - CALLS -> Function - # Function - CALLS -> Function +_CYPHER_BLOCK_RE = re.compile(r"```(?:cypher)?\s*(.*?)\s*```", re.DOTALL) - ontology.add_relation(Relation("CALLS", "Class", "Function")) - ontology.add_relation(Relation("CALLS", "Function", "Function")) - ontology.add_relation(Relation("DEFINES", "File", "Class")) - ontology.add_relation(Relation("DEFINES", "File", "Function")) - ontology.add_relation(Relation("DEFINES", "Class", "Class")) - ontology.add_relation(Relation("EXTENDS", "Class", "Class")) - ontology.add_relation(Relation("IMPLEMENTS", "Class", "Interface")) - ontology.add_relation(Relation("DEFINES", "Class", "Function")) - ontology.add_relation(Relation("DEFINES", "Function", "Function")) - return ontology +def _extract_cypher(text: str) -> str: + """Pull the Cypher statement out of an LLM response.""" + match = _CYPHER_BLOCK_RE.search(text) + if match: + return match.group(1).strip() + return text.strip() -# Global ontology -ontology = _define_ontology() -def _create_kg_agent(repo_name: str): - model_name = os.getenv('MODEL_NAME', 'gemini/gemini-flash-lite-latest') +def _build_llm() -> LiteLLM: + model_name = os.getenv("MODEL_NAME", "gemini/gemini-flash-lite-latest") + return LiteLLM(model_name, temperature=0.0) - model = LiteModel(model_name) - #ontology = _define_ontology() - code_graph_kg = KnowledgeGraph( - name=repo_name, - ontology=ontology, - model_config=KnowledgeGraphModelConfig.with_model(model), - host=os.getenv('FALKORDB_HOST', 'localhost'), - port=os.getenv('FALKORDB_PORT', 6379), - username=os.getenv('FALKORDB_USERNAME', None), - password=os.getenv('FALKORDB_PASSWORD', None), - cypher_system_instruction=CYPHER_GEN_SYSTEM, - qa_system_instruction=GRAPH_QA_SYSTEM, - cypher_gen_prompt=CYPHER_GEN_PROMPT, - qa_prompt=GRAPH_QA_PROMPT, +def _falkordb() -> AsyncFalkorDB: + return AsyncFalkorDB( + host=os.getenv("FALKORDB_HOST", "localhost"), + port=int(os.getenv("FALKORDB_PORT", "6379")), + username=os.getenv("FALKORDB_USERNAME") or None, + password=os.getenv("FALKORDB_PASSWORD") or None, ) - return code_graph_kg.chat_session() -def _ask_sync(repo_name: str, question: str) -> str: - chat = _create_kg_agent(repo_name) - - logging.debug(f"Question: {question}") - print(f"Question: {question}") - response = chat.send_message(question) - logging.debug(f"Response: {response}") - print(f"Response: {response['response']}") - return response['response'] +async def _run_cypher(repo_name: str, cypher: str) -> list[list[Any]]: + if not cypher: + return [] + db = _falkordb() + graph = db.select_graph(repo_name) + try: + result = await graph.query(cypher) + return list(result.result_set or []) + except Exception: + logger.exception("Cypher execution failed: %s", cypher) + return [] async def ask(repo_name: str, question: str) -> str: - loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, _ask_sync, repo_name, question) + """Answer a natural-language question against the code graph for repo_name.""" + llm = _build_llm() + + cypher_resp = await llm.ainvoke_messages( + [ + ChatMessage(role="system", content=CYPHER_GEN_SYSTEM.format(ontology=_ONTOLOGY_TEXT)), + ChatMessage(role="user", content=CYPHER_GEN_PROMPT.format(question=question)), + ] + ) + cypher = _extract_cypher(cypher_resp.content) + logger.debug("Generated Cypher: %s", cypher) + + context = await _run_cypher(repo_name, cypher) + + answer_resp = await llm.ainvoke_messages( + [ + ChatMessage(role="system", content=GRAPH_QA_SYSTEM), + ChatMessage( + role="user", + content=GRAPH_QA_PROMPT.format( + cypher=cypher, + context=context, + question=question, + ), + ), + ] + ) + return answer_resp.content diff --git a/pyproject.toml b/pyproject.toml index 49438fb9..a2d5f7a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{name = "Roi Lipman", email = "roilipman@gmail.com"}] readme = "README.md" requires-python = ">=3.12,<3.14" dependencies = [ - "graphrag-sdk>=0.8.1,<0.9.0", + "graphrag-sdk>=1.1.1,<2.0.0", "tree-sitter>=0.25.2,<0.26.0", "validators>=0.35.0,<0.36.0", "falkordb>=1.1.3,<2.0.0", diff --git a/uv.lock b/uv.lock index 44c773aa..7e93ea89 100644 --- a/uv.lock +++ b/uv.lock @@ -2,79 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.12, <3.14" -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -115,28 +42,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, -] - [[package]] name = "cattrs" version = "26.1.0" @@ -279,7 +184,7 @@ test = [ requires-dist = [ { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, - { name = "graphrag-sdk", specifier = ">=0.8.1,<0.9.0" }, + { name = "graphrag-sdk", specifier = ">=1.1.1,<2.0.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, { name = "multilspy", git = "https://github.com/AviAvni/multilspy.git?rev=python-init-params" }, @@ -323,12 +228,65 @@ wheels = [ ] [[package]] -name = "distro" -version = "1.9.0" +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] @@ -373,36 +331,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] -[[package]] -name = "fastuuid" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, -] - [[package]] name = "filelock" version = "3.25.0" @@ -413,69 +341,11 @@ wheels = [ ] [[package]] -name = "fix-busted-json" -version = "0.0.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/20/378d67dd0246f8d4f34902ac9a65dd81c627f77d6cea65cb21a4c34379ec/fix-busted-json-0.0.18.tar.gz", hash = "sha256:93c5dab7cae3b5d0b055f2c7043f9fe727a88a80d0be753c5f2c20bb9b69672f", size = 10491, upload-time = "2024-04-22T08:26:37.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/60/dd88b9688821079e92a0ed015779f11a65576218d525948be3148b81b86e/fix_busted_json-0.0.18-py3-none-any.whl", hash = "sha256:fdce0e02c9a810b3aa28e1c3c32c24b21b44e89f6315ec25d2b963bd52a6ef03", size = 7358, upload-time = "2024-04-22T08:26:35.946Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] @@ -487,27 +357,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "gliner" +version = "0.2.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/18/e199cb97147c4a9260c75e4caf51e17be6ff969b0604a029c9c62810cbe0/gliner-0.2.26.tar.gz", hash = "sha256:6783be92b4b81caa878dcc4269ba37800207c37118d8ff9be028b93bddd6813d", size = 181224, upload-time = "2026-03-19T15:07:22.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/6e/d54d3d2867e29b68a22b144f570c8204209647fccc7879cec5218d6ed5fb/gliner-0.2.26-py3-none-any.whl", hash = "sha256:b9baa47641efb90b9d069add0528ed2464d137991ff097f42b0cab37a91ba991", size = 170429, upload-time = "2026-03-19T15:07:19.914Z" }, +] + [[package]] name = "graphrag-sdk" -version = "0.8.2" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backoff" }, - { name = "beautifulsoup4" }, { name = "falkordb" }, - { name = "fix-busted-json" }, - { name = "litellm" }, - { name = "pypdf" }, + { name = "gliner" }, + { name = "hnswlib" }, + { name = "numpy" }, + { name = "pydantic" }, { name = "python-dotenv" }, - { name = "ratelimit" }, - { name = "rdflib" }, - { name = "requests" }, - { name = "rich" }, - { name = "typing-extensions" }, + { name = "scipy" }, + { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/7d/10caf397a39311b5bb53b715d3fec9745f3e3ff39793914d5cf308d83291/graphrag_sdk-0.8.2.tar.gz", hash = "sha256:5d9d275e1b1bc1128eb56233a07ac4a7324d95430057164629da23948c4b4b69", size = 61476, upload-time = "2026-01-15T13:28:13.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/73/f63b7e440ca6456a166c23f8603f3e1f7ca0eedce773250722c647886116/graphrag_sdk-1.1.1.tar.gz", hash = "sha256:e8abd863cfb5dccd14a897967553d8085657202ba1a1742f4dfdc4faafd6d78d", size = 218977, upload-time = "2026-05-13T13:17:43.04Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/10/ea2845cab0288f962ee709ccb066f1476d2251e12feaa5262adb62311ec6/graphrag_sdk-0.8.2-py3-none-any.whl", hash = "sha256:f77a0addd08c13f89bbb87cd3b9a432729e1808bcabf2adeef79d8df3897e0e7", size = 83541, upload-time = "2026-01-15T13:28:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/29b5d7d94c077b0ec2254366b4bdf8a8a032240098c82909f4b1a4f99225/graphrag_sdk-1.1.1-py3-none-any.whl", hash = "sha256:4f18a833f39854883875e11c981bad5649a0f467280a19702e82b1c9938a7abe", size = 159565, upload-time = "2026-05-13T13:17:40.963Z" }, ] [[package]] @@ -543,6 +426,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, ] +[[package]] +name = "hnswlib" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/7a/1a9b1405f2eb59515f06c3074750b03e0e96edf7fee0f6dd6df81d9c21d7/hnswlib-0.8.0.tar.gz", hash = "sha256:cb6d037eedebb34a7134e7dc78966441dfd04c9cf5ee93911be911ced951c44c", size = 36206, upload-time = "2023-12-03T04:16:17.55Z" } + [[package]] name = "httpcore" version = "1.0.9" @@ -695,99 +587,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "litellm" -version = "1.82.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/00/49bb5c28e0dea0f5086229a2a08d5fdc6c8dc0d8e2acb2a2d1f7dd9f4b70/litellm-1.82.0.tar.gz", hash = "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22", size = 16800355, upload-time = "2026-03-01T02:35:30.363Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/89/eb28bfcf97d6b045c400e72eb047c381594467048c237dbb6c227764084c/litellm-1.82.0-py3-none-any.whl", hash = "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", size = 14911978, upload-time = "2026-03-01T02:35:26.844Z" }, -] - [[package]] name = "lsprotocol" version = "2023.0.1" @@ -864,66 +663,12 @@ wheels = [ ] [[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] @@ -936,22 +681,229 @@ dependencies = [ ] [[package]] -name = "openai" -version = "2.26.0" +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/b1/d111b1df656761f980d9e298a60039a9cb66036b1d039e777537743d0ac3/onnxruntime-1.26.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05b028781b322ad74b57ce5b50aa5280bb1fe96ceec334628ade681e0b24c1ac", size = 18016624, upload-time = "2026-05-12T00:41:01.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/3f9d896a0385a36bd04345d6d0b802821a5782adde562e7e135f6bb71c73/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f2bb870a4b9224eba0a6728c1fa7a9e552b8e59e1083c51fbbc3d013f2b5c0", size = 16052692, upload-time = "2026-05-08T19:07:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/2a4e04f8dbeffad19bbcced4bcd4289bf478921518437404d6b92bdf213b/onnxruntime-1.26.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b6dd70599005bd1bf29779f04a91978b92b5e719c11a20068a8f8e535f725b6", size = 18185439, upload-time = "2026-05-08T19:07:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/44/fc/026d0a7162b9c2153dac292baea9e027c42304dc1d9dc6f8ff5b4cfbaedd/onnxruntime-1.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:a26374dc7fbcaae593601086b242120e13f2310558df0991da6dd8b8fac00414", size = 13026427, upload-time = "2026-05-08T19:08:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/1dcf88e45e4c69db5f7b106f2dacc3801ba98994e082ca03e1dfdf7bfe57/onnxruntime-1.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:54a8053410fd31fd66469bd754fcfe8a4df9f7eb44756b4b5479bf50c842d948", size = 12796647, upload-time = "2026-05-08T19:07:52.108Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, ] [[package]] @@ -982,57 +934,18 @@ wheels = [ ] [[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +name = "protobuf" +version = "7.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/fd/5b1491d9e4b586d621c54f4c36b888714164b6875f8d6afa3f9072906a51/protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6", size = 458677, upload-time = "2026-05-19T23:02:29.197Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ee/93d06e358a4aa32280b00e722d3ea0a1f25fc3cc5778d80581c9cca2c10e/protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda", size = 433225, upload-time = "2026-05-19T23:02:19.884Z" }, + { url = "https://files.pythonhosted.org/packages/8b/39/1c76c2da93f3c507e958e0aecee2391cc44d4625de6c728bbc555195b5a8/protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5", size = 328847, upload-time = "2026-05-19T23:02:22.3Z" }, + { url = "https://files.pythonhosted.org/packages/91/1a/39f7ce90a238c1a987a4d81ec26379e02ca0aff367de68e4a1fa474215b9/protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee", size = 344030, upload-time = "2026-05-19T23:02:23.591Z" }, + { url = "https://files.pythonhosted.org/packages/70/5b/6baf9008817964454055ff3fe65f1de0b5f1e26c80c82f7fb108b7cd4ea3/protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011", size = 327130, upload-time = "2026-05-19T23:02:24.637Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e5/e46adb0badc388bfb84877a5f9f026aff63f60e611016cf64dbe77e05446/protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6", size = 428946, upload-time = "2026-05-19T23:02:25.741Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ab/547fbd9e16d879dd13c167478f8ae0a83a428008ca07a5e06acdc23ad473/protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201", size = 439996, upload-time = "2026-05-19T23:02:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ef/50433d346c56657a70d27f156c7b349ac59a068b01de4eb796e747eecc43/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0", size = 171659, upload-time = "2026-05-19T23:02:27.842Z" }, ] [[package]] @@ -1155,24 +1068,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - -[[package]] -name = "pypdf" -version = "5.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/3a/584b97a228950ed85aec97c811c68473d9b8d149e6a8c155668287cf1a28/pypdf-5.9.0.tar.gz", hash = "sha256:30f67a614d558e495e1fbb157ba58c1de91ffc1718f5e0dfeb82a029233890a1", size = 5035118, upload-time = "2025-07-27T14:04:52.364Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/d9/6cff57c80a6963e7dd183bf09e9f21604a77716644b1e580e97b259f7612/pypdf-5.9.0-py3-none-any.whl", hash = "sha256:be10a4c54202f46d9daceaa8788be07aa8cd5ea8c25c529c50dd509206382c35", size = 313193, upload-time = "2025-07-27T14:04:50.53Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -1238,24 +1133,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] -[[package]] -name = "ratelimit" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/38/ff60c8fc9e002d50d48822cc5095deb8ebbc5f91a6b8fdd9731c87a147c9/ratelimit-2.2.1.tar.gz", hash = "sha256:af8a9b64b821529aca09ebaf6d8d279100d766f19e90b5059ac6a718ca6dee42", size = 5251, upload-time = "2018-12-17T18:55:49.675Z" } - -[[package]] -name = "rdflib" -version = "7.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" }, -] - [[package]] name = "redis" version = "7.3.0" @@ -1265,20 +1142,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "regex" version = "2026.2.28" @@ -1363,58 +1226,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, -] - [[package]] name = "ruff" version = "0.15.5" @@ -1441,39 +1252,125 @@ wheels = [ ] [[package]] -name = "shellingham" -version = "1.5.4" +name = "safetensors" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, ] [[package]] -name = "six" -version = "1.17.0" +name = "scipy" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] -name = "sniffio" -version = "1.3.1" +name = "shellingham" +version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] -name = "soupsieve" -version = "2.8.3" +name = "six" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -1489,6 +1386,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -1548,6 +1457,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] +[[package]] +name = "torch" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" }, + { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" }, + { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" }, +] + [[package]] name = "tqdm" version = "4.67.3" @@ -1560,6 +1505,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] +[[package]] +name = "transformers" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer-slim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/1d/a7d91500a6c02ec76058bc9e65fcdec1bdb8882854dec8e4adf12d0aa8b0/transformers-5.1.0.tar.gz", hash = "sha256:c60d6180e5845ea1b4eed38d7d1b06fcc4cc341c6b7fa5c1dc767d7e25fe0139", size = 8531810, upload-time = "2026-02-05T15:41:42.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/66/57042d4b0f1ede8046d7ae6409bf3640df996e9cbc3fe20467aa29badc54/transformers-5.1.0-py3-none-any.whl", hash = "sha256:de534b50c9b2ce6217fc56421075a1734241fb40704fdc90f50f6a08fc533d59", size = 10276537, upload-time = "2026-02-05T15:41:40.358Z" }, +] + [[package]] name = "tree-sitter" version = "0.25.2" @@ -1603,6 +1568,14 @@ version = "0.23.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/dc/d4a0ad9e466263728f80f9dac399609473af01c1aba2ea3ea8879ce56276/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c", size = 333661, upload-time = "2026-04-14T15:11:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/61/7a/5c862770460a2e27079e725585ad2718100373c09448c14e36934ef44414/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c", size = 376295, upload-time = "2026-04-14T15:11:15.346Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/0571a3a34c0feda60a9c37cf6dd5edfdbc24f8fcb1e48b6b6eb0f324ad2a/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5", size = 358331, upload-time = "2026-04-14T15:11:16.418Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/0f7e1f50f6365338eb700f01710da0adc49a49fa9a8443e5a90ea4f29491/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf", size = 359444, upload-time = "2026-04-14T15:11:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/129bd56d5ef22b4ae254940a09b6d3ed873093218868a3f9635d571d514e/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832", size = 358143, upload-time = "2026-04-14T15:11:18.755Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cd/e12cdca47e0c56151cb4b156d48091b7bc1d968e072c1656cf6b73fe7218/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e", size = 357524, upload-time = "2026-04-14T15:11:19.717Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/f742d60f818cba83760f4975c7158d1c96c36b5807e95a843db7fb8c64b7/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be", size = 338755, upload-time = "2026-04-14T15:11:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/8a8642b9bba86248ac2facc81ffb187c06c6768efa56c79d61fab70d736b/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345", size = 337261, upload-time = "2026-04-14T15:11:22.111Z" }, { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, @@ -1643,6 +1616,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, ] +[[package]] +name = "triton" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" }, + { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" }, +] + [[package]] name = "typer" version = "0.24.1" @@ -1658,6 +1644,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1815,74 +1813,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -] - [[package]] name = "zipp" version = "3.23.0" From aed0c567e8f6a90135f7ea669e17a1d4426b9309 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 21:45:58 +0300 Subject: [PATCH 13/63] Add benchmark metrics + report aggregation modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench/metrics/ parses SWE-agent trajectory JSON into per-task TaskMetrics rows: input/output tokens, tool-call counts (with per-tool breakdown), patch, outcome. Defensive about trajectory-shape drift between SWE-agent versions (history vs trajectory vs steps; openai-style tool_calls vs SWE-agent action.command). bench/report/ aggregates those rows into a per-config table with median + p90 tokens and Δ-vs-baseline. The summary picks the best run per task (resolved > failed) so retries don't double-count. 10 unit tests cover token extraction, both tool-call shapes, the retry-merge rule, and the markdown delta column. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/metrics/__init__.py | 178 ++++++++++++++++++++++++++++++++++++ bench/report/__init__.py | 128 ++++++++++++++++++++++++++ tests/test_bench_metrics.py | 165 +++++++++++++++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 bench/metrics/__init__.py create mode 100644 bench/report/__init__.py create mode 100644 tests/test_bench_metrics.py diff --git a/bench/metrics/__init__.py b/bench/metrics/__init__.py new file mode 100644 index 00000000..22788381 --- /dev/null +++ b/bench/metrics/__init__.py @@ -0,0 +1,178 @@ +"""Trajectory parsing and per-task metrics for the benchmark. + +SWE-agent emits a trajectory JSON per task. We extract the headline numbers: + +- token usage (input + output, per-call sum) +- tool calls (count + per-tool breakdown) +- patch (the agent's final diff submission) +- outcome (resolved / failed / budget_exceeded), evaluated externally + by SWE-bench's official scorer + +The functions in this module are pure and trajectory-shape-agnostic to the +extent possible: each is defensive about missing/renamed fields because +SWE-agent's exact JSON schema has drifted between versions. The tests in +`tests/test_bench_metrics.py` lock the contract for a small synthetic +trajectory. +""" + +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any + + +@dataclass +class TaskMetrics: + """One row of the results JSONL: per-task per-config measurement.""" + + benchmark: str # "swe_bench_verified" + task_id: str # e.g. "django__django-12345" + config: str # "baseline" | "lsp" | "code_graph" + run_idx: int # 0 for pass@1, 1+ for retries + + # token cost (LLM only — never combined with indexing) + input_tokens: int + output_tokens: int + + # tool-call sanity check + tool_calls_total: int + tool_calls_by_name: dict[str, int] = field(default_factory=dict) + + # outcome (set after scoring; None until then) + outcome: str | None = None # "resolved" | "failed" | "budget_exceeded" | "error" + patch: str | None = None + wall_clock_sec: float | None = None + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +_TOKEN_KEYS_IN = ("prompt_tokens", "input_tokens", "tokens_in") +_TOKEN_KEYS_OUT = ("completion_tokens", "output_tokens", "tokens_out") + + +def _first_int(d: dict[str, Any], keys: tuple[str, ...]) -> int: + for k in keys: + v = d.get(k) + if isinstance(v, int): + return v + return 0 + + +def _iter_history_steps(traj: dict[str, Any]) -> list[dict[str, Any]]: + """SWE-agent has flipped between top-level `history`, `trajectory`, and + `steps`. Return whichever list is present, else []. + """ + for key in ("history", "trajectory", "steps"): + v = traj.get(key) + if isinstance(v, list): + return v + return [] + + +def extract_token_usage(traj: dict[str, Any]) -> tuple[int, int]: + """Sum input + output tokens across all LLM calls in the trajectory. + + Looks for `usage` sub-objects on each step (the conventional shape for + OpenAI/Anthropic-style responses passed through LiteLLM). + """ + total_in = 0 + total_out = 0 + for step in _iter_history_steps(traj): + usage = step.get("usage") if isinstance(step, dict) else None + if isinstance(usage, dict): + total_in += _first_int(usage, _TOKEN_KEYS_IN) + total_out += _first_int(usage, _TOKEN_KEYS_OUT) + # Fall back to a top-level summary if present. + summary = traj.get("total_usage") or traj.get("usage") + if (total_in == 0 and total_out == 0) and isinstance(summary, dict): + total_in = _first_int(summary, _TOKEN_KEYS_IN) + total_out = _first_int(summary, _TOKEN_KEYS_OUT) + return total_in, total_out + + +def extract_tool_calls(traj: dict[str, Any]) -> tuple[int, dict[str, int]]: + """Count every tool/action invocation and bucket by tool name.""" + by_name: dict[str, int] = {} + total = 0 + for step in _iter_history_steps(traj): + if not isinstance(step, dict): + continue + # SWE-agent shapes: action.command, tool_calls[*].function.name, action.name + name = None + action = step.get("action") + if isinstance(action, dict): + name = action.get("command") or action.get("name") or action.get("tool") + if not name: + tcs = step.get("tool_calls") + if isinstance(tcs, list) and tcs: + fn = tcs[0].get("function") if isinstance(tcs[0], dict) else None + if isinstance(fn, dict): + name = fn.get("name") + if not name: + continue + by_name[name] = by_name.get(name, 0) + 1 + total += 1 + return total, by_name + + +def extract_patch(traj: dict[str, Any]) -> str | None: + """The final diff the agent submitted, if any.""" + for key in ("model_patch", "submission", "patch"): + v = traj.get(key) + if isinstance(v, str) and v.strip(): + return v + info = traj.get("info") if isinstance(traj.get("info"), dict) else None + if info: + for key in ("model_patch", "submission", "patch"): + v = info.get(key) + if isinstance(v, str) and v.strip(): + return v + return None + + +def task_metrics_from_trajectory( + traj: dict[str, Any], + *, + benchmark: str, + task_id: str, + config: str, + run_idx: int = 0, + wall_clock_sec: float | None = None, +) -> TaskMetrics: + tin, tout = extract_token_usage(traj) + n_calls, by_name = extract_tool_calls(traj) + return TaskMetrics( + benchmark=benchmark, + task_id=task_id, + config=config, + run_idx=run_idx, + input_tokens=tin, + output_tokens=tout, + tool_calls_total=n_calls, + tool_calls_by_name=by_name, + patch=extract_patch(traj), + wall_clock_sec=wall_clock_sec, + ) + + +def load_trajectory(path: str | Path) -> dict[str, Any]: + with open(path) as f: + return json.load(f) + + +def write_jsonl(path: str | Path, rows: list[TaskMetrics]) -> None: + p = Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, "w") as f: + for r in rows: + f.write(json.dumps(r.to_dict()) + "\n") + + +def append_jsonl(path: str | Path, row: TaskMetrics) -> None: + p = Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, "a") as f: + f.write(json.dumps(row.to_dict()) + "\n") diff --git a/bench/report/__init__.py b/bench/report/__init__.py new file mode 100644 index 00000000..4b304fde --- /dev/null +++ b/bench/report/__init__.py @@ -0,0 +1,128 @@ +"""Aggregate per-task metrics into a per-config summary table. + +Reads the JSONL emitted by bench/metrics and produces: +- a markdown table grouped by (benchmark, config) with accuracy + tokens +- a token-savings Δ column vs the baseline config +""" + +from __future__ import annotations + +import json +import statistics +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass +class ConfigSummary: + benchmark: str + config: str + n_tasks: int + n_resolved: int + median_tokens: int + p90_tokens: int + + @property + def resolve_rate(self) -> float: + return self.n_resolved / self.n_tasks if self.n_tasks else 0.0 + + @property + def total_tokens(self) -> int: + return self.median_tokens * self.n_tasks # not perfectly accurate, replaced below + + +def _percentile(values: list[int], p: float) -> int: + if not values: + return 0 + s = sorted(values) + k = (len(s) - 1) * p + f = int(k) + c = min(f + 1, len(s) - 1) + if f == c: + return s[f] + return int(s[f] + (s[c] - s[f]) * (k - f)) + + +def load_jsonl(path: str | Path) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + with open(path) as f: + for line in f: + line = line.strip() + if line: + rows.append(json.loads(line)) + return rows + + +def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: + by_key: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) + for r in rows: + by_key[(r["benchmark"], r["config"])].append(r) + + summaries: list[ConfigSummary] = [] + for (bench, cfg), task_rows in sorted(by_key.items()): + # Pick the best run per (task_id) — pass@1 with retries. + best_by_task: dict[str, dict[str, Any]] = {} + for r in task_rows: + tid = r["task_id"] + prev = best_by_task.get(tid) + if prev is None: + best_by_task[tid] = r + continue + # prefer a resolved outcome + if prev.get("outcome") != "resolved" and r.get("outcome") == "resolved": + best_by_task[tid] = r + + token_sums = [ + (r["input_tokens"] + r["output_tokens"]) for r in best_by_task.values() + ] + n_resolved = sum(1 for r in best_by_task.values() if r.get("outcome") == "resolved") + summaries.append( + ConfigSummary( + benchmark=bench, + config=cfg, + n_tasks=len(best_by_task), + n_resolved=n_resolved, + median_tokens=int(statistics.median(token_sums)) if token_sums else 0, + p90_tokens=_percentile(token_sums, 0.9), + ) + ) + return summaries + + +def render_markdown(summaries: list[ConfigSummary]) -> str: + lines: list[str] = [] + lines.append("# Benchmark results\n") + by_bench: dict[str, list[ConfigSummary]] = defaultdict(list) + for s in summaries: + by_bench[s.benchmark].append(s) + + for bench, group in sorted(by_bench.items()): + baseline = next((s for s in group if s.config == "baseline"), None) + baseline_med = baseline.median_tokens if baseline else 0 + + lines.append(f"## {bench}\n") + lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline |") + lines.append("|---|---:|---:|---:|---:|---:|---:|") + for s in sorted(group, key=lambda x: x.config): + delta = "—" + if baseline_med and s.config != "baseline": + pct = (s.median_tokens - baseline_med) / baseline_med * 100 + delta = f"{pct:+.1f}%" + lines.append( + f"| {s.config} | {s.n_tasks} | {s.n_resolved} | " + f"{s.resolve_rate * 100:.1f}% | {s.median_tokens:,} | " + f"{s.p90_tokens:,} | {delta} |" + ) + lines.append("") + return "\n".join(lines) + + +def aggregate_to_markdown(jsonl_path: str | Path, out_path: str | Path) -> None: + rows = load_jsonl(jsonl_path) + summaries = summarize(rows) + md = render_markdown(summaries) + out = Path(out_path) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(md) diff --git a/tests/test_bench_metrics.py b/tests/test_bench_metrics.py new file mode 100644 index 00000000..f34fd967 --- /dev/null +++ b/tests/test_bench_metrics.py @@ -0,0 +1,165 @@ +"""Tests for bench/metrics and bench/report — pure-function units.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from bench.metrics import ( + TaskMetrics, + extract_patch, + extract_token_usage, + extract_tool_calls, + task_metrics_from_trajectory, + write_jsonl, +) +from bench.report import aggregate_to_markdown, render_markdown, summarize + + +_TRAJ_FIXTURE = { + "history": [ + { + "action": {"command": "read_file"}, + "usage": {"prompt_tokens": 1200, "completion_tokens": 80}, + }, + { + "action": {"command": "graph_entities"}, + "usage": {"prompt_tokens": 800, "completion_tokens": 40}, + }, + { + "action": {"command": "read_file"}, + "usage": {"prompt_tokens": 1500, "completion_tokens": 90}, + }, + { + "action": {"command": "submit"}, + "usage": {"prompt_tokens": 200, "completion_tokens": 30}, + }, + ], + "model_patch": "diff --git a/x b/x\n--- a/x\n+++ b/x\n@@\n-foo\n+bar\n", +} + + +def test_extract_token_usage_sums_history(): + tin, tout = extract_token_usage(_TRAJ_FIXTURE) + assert tin == 1200 + 800 + 1500 + 200 + assert tout == 80 + 40 + 90 + 30 + + +def test_extract_token_usage_falls_back_to_summary(): + traj = {"total_usage": {"input_tokens": 5000, "output_tokens": 300}} + tin, tout = extract_token_usage(traj) + assert (tin, tout) == (5000, 300) + + +def test_extract_tool_calls_buckets_by_name(): + total, by_name = extract_tool_calls(_TRAJ_FIXTURE) + assert total == 4 + assert by_name == {"read_file": 2, "graph_entities": 1, "submit": 1} + + +def test_extract_tool_calls_supports_openai_shape(): + traj = { + "history": [ + {"tool_calls": [{"function": {"name": "find_references"}}]}, + {"tool_calls": [{"function": {"name": "find_references"}}]}, + {"tool_calls": [{"function": {"name": "hover"}}]}, + ] + } + total, by_name = extract_tool_calls(traj) + assert total == 3 + assert by_name == {"find_references": 2, "hover": 1} + + +def test_extract_patch(): + assert extract_patch(_TRAJ_FIXTURE).startswith("diff --git") + assert extract_patch({}) is None + + +def test_task_metrics_from_trajectory_round_trip(): + m = task_metrics_from_trajectory( + _TRAJ_FIXTURE, + benchmark="swe_bench_verified", + task_id="django__django-1", + config="code_graph", + ) + assert m.task_id == "django__django-1" + assert m.config == "code_graph" + assert m.input_tokens == 3700 + assert m.output_tokens == 240 + assert m.tool_calls_total == 4 + d = m.to_dict() + assert json.loads(json.dumps(d))["task_id"] == "django__django-1" + + +def _row(config: str, tokens: int, resolved: bool, task_id: str = "t1") -> dict: + return { + "benchmark": "swe_bench_verified", + "task_id": task_id, + "config": config, + "run_idx": 0, + "input_tokens": tokens // 2, + "output_tokens": tokens // 2, + "tool_calls_total": 1, + "tool_calls_by_name": {}, + "outcome": "resolved" if resolved else "failed", + "patch": "diff" if resolved else None, + "wall_clock_sec": None, + } + + +def test_summarize_resolve_rate_and_medians(): + rows = [ + _row("baseline", 1000, True, "t1"), + _row("baseline", 2000, False, "t2"), + _row("baseline", 3000, True, "t3"), + _row("code_graph", 800, True, "t1"), + _row("code_graph", 900, True, "t2"), + _row("code_graph", 1100, False, "t3"), + ] + summaries = summarize(rows) + s = {s.config: s for s in summaries} + + assert s["baseline"].n_tasks == 3 + assert s["baseline"].n_resolved == 2 + assert pytest.approx(s["baseline"].resolve_rate) == 2 / 3 + assert s["baseline"].median_tokens == 2000 + + assert s["code_graph"].n_resolved == 2 + assert s["code_graph"].median_tokens == 900 + + +def test_summarize_prefers_resolved_run_for_retries(): + rows = [ + {**_row("lsp", 500, False, "t1"), "run_idx": 0}, + {**_row("lsp", 700, True, "t1"), "run_idx": 1}, + ] + s = summarize(rows)[0] + assert s.n_resolved == 1 + assert s.median_tokens == 700 + + +def test_render_markdown_includes_delta(): + rows = [ + _row("baseline", 2000, True, "t1"), + _row("baseline", 4000, True, "t2"), + _row("code_graph", 1000, True, "t1"), + _row("code_graph", 1200, True, "t2"), + ] + md = render_markdown(summarize(rows)) + assert "baseline" in md + assert "code_graph" in md + assert "Δ tokens vs baseline" in md + # code_graph median (1100) vs baseline median (3000) → ~-63% + assert "-63.3%" in md + + +def test_aggregate_to_markdown_writes_file(tmp_path: Path): + rows = [_row("baseline", 1000, True), _row("lsp", 800, True)] + jsonl = tmp_path / "results.jsonl" + write_jsonl(jsonl, [TaskMetrics(**r) for r in rows]) + out = tmp_path / "results.md" + aggregate_to_markdown(jsonl, out) + assert out.exists() + assert "lsp" in out.read_text() From c52f6e6d15791a80bfc5fe9e83fde8ed47dc6ca1 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 21:47:37 +0300 Subject: [PATCH 14/63] Add code-graph HTTP adapter for the benchmark agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench/agents/code_graph_adapter.py exposes the seven tools the code-graph SWE-agent config gets: - graph_entities, get_neighbors, find_paths, auto_complete: thin wrappers over the existing FastAPI surface. - find_symbol: exact-name lookup, built client-side on top of auto_complete so we don't grow the server surface. - note_edit: incremental re-index hook the agent must call after every write_file/edit. Currently routes through analyze_folder on the dirname; degrades gracefully if the call fails. Crucially, GraphRAG is NOT exposed (Q2 grill decision: nested-agent double-counting). Both class-style (CodeGraphClient context manager) and function-style (graph_entities(...) etc.) are provided — the function form is what SWE-agent's tool registry needs. 9 unit tests using httpx.MockTransport cover all seven methods, the bearer-token auth header, 4xx propagation, and note_edit's non-fatal failure path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/__init__.py | 0 bench/agents/code_graph_adapter.py | 166 +++++++++++++++++++++++++ tests/test_bench_code_graph_adapter.py | 116 +++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 bench/agents/__init__.py create mode 100644 bench/agents/code_graph_adapter.py create mode 100644 tests/test_bench_code_graph_adapter.py diff --git a/bench/agents/__init__.py b/bench/agents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bench/agents/code_graph_adapter.py b/bench/agents/code_graph_adapter.py new file mode 100644 index 00000000..f3e6e633 --- /dev/null +++ b/bench/agents/code_graph_adapter.py @@ -0,0 +1,166 @@ +"""HTTP adapter to code-graph's FastAPI service for the benchmark. + +The benchmark agent runs inside SWE-agent's container; this module is the +in-container tool surface for the `code_graph` config. Each public function +maps 1:1 onto an SWE-agent tool name registered in +`bench/tools/code_graph/tools.yaml`. + +We talk to the host-side code-graph service over HTTP (see backend.service_url +in tools.yaml). The five primitives below — plus `find_symbol` and +`note_edit` — are the only tools the code-graph config gets. The GraphRAG +`chat` endpoint is intentionally NOT exposed (Q2 grill decision). + +This module is pure-HTTP (httpx). It does no model calls and no graph +traversal of its own. Token cost on the code-graph side comes only from +the agent's LLM reading these tool responses. +""" + +from __future__ import annotations + +import os +from typing import Any + +import httpx + + +DEFAULT_TIMEOUT_SEC = 30.0 + + +class CodeGraphClient: + """Thin synchronous client. SWE-agent tools are typically sync.""" + + def __init__( + self, + base_url: str | None = None, + token: str | None = None, + timeout: float = DEFAULT_TIMEOUT_SEC, + *, + transport: httpx.BaseTransport | None = None, + ) -> None: + self.base_url = (base_url or os.getenv("CODEGRAPH_URL") + or "http://host.docker.internal:5000").rstrip("/") + self.token = token or os.getenv("SECRET_TOKEN") or os.getenv("CODEGRAPH_TOKEN") + headers = {} + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + self._client = httpx.Client( + base_url=self.base_url, + headers=headers, + timeout=timeout, + transport=transport, + ) + + def close(self) -> None: + self._client.close() + + def __enter__(self) -> CodeGraphClient: + return self + + def __exit__(self, *exc: Any) -> None: + self.close() + + # ------------------------------------------------------------------ + # Primitive tools — names match bench/tools/code_graph/tools.yaml + # ------------------------------------------------------------------ + + def graph_entities(self, repo: str) -> dict[str, Any]: + """Fetch the sub-graph for a repo (paginated server-side).""" + r = self._client.get("/api/graph_entities", params={"repo": repo}) + r.raise_for_status() + return r.json() + + def get_neighbors(self, repo: str, node_ids: list[int]) -> dict[str, Any]: + r = self._client.post( + "/api/get_neighbors", + json={"repo": repo, "node_ids": list(node_ids)}, + ) + r.raise_for_status() + return r.json() + + def find_paths(self, repo: str, src: int, dest: int) -> dict[str, Any]: + r = self._client.post( + "/api/find_paths", + json={"repo": repo, "src": src, "dest": dest}, + ) + r.raise_for_status() + return r.json() + + def auto_complete(self, repo: str, prefix: str) -> dict[str, Any]: + r = self._client.post( + "/api/auto_complete", + json={"repo": repo, "prefix": prefix}, + ) + r.raise_for_status() + return r.json() + + # ------------------------------------------------------------------ + # Helper tools — bench-only, not part of the FastAPI surface + # ------------------------------------------------------------------ + + def find_symbol(self, repo: str, name: str) -> list[dict[str, Any]]: + """Exact-name lookup. Thin wrapper around auto_complete + filter. + + auto_complete returns prefix matches; the agent often wants exact + matches. Doing this client-side keeps the FastAPI surface untouched. + """ + payload = self.auto_complete(repo, name) + results = payload.get("completions") or payload.get("results") or payload + if isinstance(results, dict): + results = results.get("items", []) + return [ + item for item in (results or []) + if isinstance(item, dict) and item.get("name") == name + ] + + def note_edit(self, repo: str, path: str) -> dict[str, Any]: + """Tell code-graph the agent just edited `path`; trigger an + incremental re-index of that single file. + + Implemented by hitting analyze_folder with the file's directory. + Until the backend grows a true single-file endpoint this is the + cheapest available approximation. Failures are non-fatal — the + agent should continue and accept a slightly stale graph. + """ + try: + r = self._client.post( + "/api/analyze_folder", + json={"path": os.path.dirname(path) or path, "ignore": []}, + ) + r.raise_for_status() + return {"ok": True, "reindexed": path} + except httpx.HTTPError as exc: + return {"ok": False, "error": str(exc), "path": path} + + +# Convenience function aliases — the SWE-agent tool registry expects +# top-level callables. Each spins up a short-lived client; for hot loops +# the agent should keep a CodeGraphClient open instead. + +def graph_entities(repo: str, **kw: Any) -> dict[str, Any]: + with CodeGraphClient(**kw) as c: + return c.graph_entities(repo) + + +def get_neighbors(repo: str, node_ids: list[int], **kw: Any) -> dict[str, Any]: + with CodeGraphClient(**kw) as c: + return c.get_neighbors(repo, node_ids) + + +def find_paths(repo: str, src: int, dest: int, **kw: Any) -> dict[str, Any]: + with CodeGraphClient(**kw) as c: + return c.find_paths(repo, src, dest) + + +def auto_complete(repo: str, prefix: str, **kw: Any) -> dict[str, Any]: + with CodeGraphClient(**kw) as c: + return c.auto_complete(repo, prefix) + + +def find_symbol(repo: str, name: str, **kw: Any) -> list[dict[str, Any]]: + with CodeGraphClient(**kw) as c: + return c.find_symbol(repo, name) + + +def note_edit(repo: str, path: str, **kw: Any) -> dict[str, Any]: + with CodeGraphClient(**kw) as c: + return c.note_edit(repo, path) diff --git a/tests/test_bench_code_graph_adapter.py b/tests/test_bench_code_graph_adapter.py new file mode 100644 index 00000000..192695ed --- /dev/null +++ b/tests/test_bench_code_graph_adapter.py @@ -0,0 +1,116 @@ +"""Tests for the code-graph HTTP adapter using httpx MockTransport.""" + +from __future__ import annotations + +import json + +import httpx +import pytest + +from bench.agents.code_graph_adapter import CodeGraphClient + + +def _mock_handler_factory(responses: dict[str, httpx.Response]): + """Build a MockTransport handler keyed by 'METHOD path'.""" + + def handler(request: httpx.Request) -> httpx.Response: + key = f"{request.method} {request.url.path}" + if key not in responses: + return httpx.Response(404, json={"error": f"no mock for {key}"}) + resp = responses[key] + # capture the body for assertion via the response.extensions + resp.extensions["last_request_body"] = ( + json.loads(request.content) if request.content else None + ) + resp.extensions["last_request_query"] = dict(request.url.params) + return resp + + return handler + + +def _client_with(responses: dict[str, httpx.Response]) -> CodeGraphClient: + transport = httpx.MockTransport(_mock_handler_factory(responses)) + return CodeGraphClient( + base_url="http://test", token="t0k3n", transport=transport + ) + + +def test_graph_entities_sends_repo_query_and_returns_json(): + resp = httpx.Response(200, json={"nodes": [{"id": 1}], "edges": []}) + with _client_with({"GET /api/graph_entities": resp}) as c: + out = c.graph_entities("my-repo") + assert out == {"nodes": [{"id": 1}], "edges": []} + assert resp.extensions["last_request_query"] == {"repo": "my-repo"} + + +def test_get_neighbors_posts_json_body(): + resp = httpx.Response(200, json={"nodes": [], "edges": []}) + with _client_with({"POST /api/get_neighbors": resp}) as c: + c.get_neighbors("r", [1, 2, 3]) + assert resp.extensions["last_request_body"] == {"repo": "r", "node_ids": [1, 2, 3]} + + +def test_find_paths_posts_src_dest(): + resp = httpx.Response(200, json={"paths": []}) + with _client_with({"POST /api/find_paths": resp}) as c: + c.find_paths("r", 7, 42) + assert resp.extensions["last_request_body"] == {"repo": "r", "src": 7, "dest": 42} + + +def test_auto_complete_posts_prefix(): + resp = httpx.Response(200, json={"completions": [{"name": "Foo"}, {"name": "FooBar"}]}) + with _client_with({"POST /api/auto_complete": resp}) as c: + out = c.auto_complete("r", "Foo") + assert resp.extensions["last_request_body"] == {"repo": "r", "prefix": "Foo"} + assert "completions" in out + + +def test_find_symbol_filters_for_exact_match(): + resp = httpx.Response( + 200, + json={"completions": [ + {"name": "Foo", "id": 1}, + {"name": "FooBar", "id": 2}, + {"name": "Foo", "id": 3}, + ]}, + ) + with _client_with({"POST /api/auto_complete": resp}) as c: + out = c.find_symbol("r", "Foo") + assert len(out) == 2 + assert all(item["name"] == "Foo" for item in out) + + +def test_note_edit_calls_analyze_folder_and_reports_path(): + resp = httpx.Response(200, json={"status": "ok"}) + with _client_with({"POST /api/analyze_folder": resp}) as c: + out = c.note_edit("r", "/work/repo/x/y.py") + assert out == {"ok": True, "reindexed": "/work/repo/x/y.py"} + assert resp.extensions["last_request_body"]["path"] == "/work/repo/x" # os.path.dirname of /work/repo/x/y.py + + +def test_note_edit_returns_error_dict_on_http_failure(): + resp = httpx.Response(500, json={"error": "boom"}) + with _client_with({"POST /api/analyze_folder": resp}) as c: + out = c.note_edit("r", "/work/repo/x.py") + assert out["ok"] is False + assert "error" in out + + +def test_client_sends_bearer_token(): + captured = {} + + def handler(request: httpx.Request) -> httpx.Response: + captured["auth"] = request.headers.get("authorization") + return httpx.Response(200, json={}) + + transport = httpx.MockTransport(handler) + with CodeGraphClient(base_url="http://test", token="secret", transport=transport) as c: + c.graph_entities("r") + assert captured["auth"] == "Bearer secret" + + +def test_4xx_raises(): + resp = httpx.Response(401, json={"detail": "Unauthorized"}) + with _client_with({"GET /api/graph_entities": resp}) as c: + with pytest.raises(httpx.HTTPStatusError): + c.graph_entities("r") From 20ba1edaaa1d9b9fbba192cb7189c3eda2973c8f Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 21:48:25 +0300 Subject: [PATCH 15/63] Add indexing-cache registry for benchmark runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench/runners/index_cache.py tracks which @ pairs code-graph has already analyzed, so re-running the benchmark doesn't pay the indexing cost twice. Backed by a single JSON file under bench/cache/. Atomic via tmp-file replace. This module doesn't run analysis itself — that's done via code-graph's existing /api/analyze_folder endpoint. This is just the bookkeeping the runner consults before deciding to re-index. 6 unit tests cover record/lookup, cross-instance persistence, forget, and overwrite semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/__init__.py | 0 bench/runners/index_cache.py | 83 +++++++++++++++++++++++++++++++++ tests/test_bench_index_cache.py | 58 +++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 bench/runners/__init__.py create mode 100644 bench/runners/index_cache.py create mode 100644 tests/test_bench_index_cache.py diff --git a/bench/runners/__init__.py b/bench/runners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bench/runners/index_cache.py b/bench/runners/index_cache.py new file mode 100644 index 00000000..d51605ec --- /dev/null +++ b/bench/runners/index_cache.py @@ -0,0 +1,83 @@ +"""Indexing-cache registry — track which `@` pairs have been +indexed by code-graph, so the benchmark runner doesn't re-index between runs. + +This is a tiny JSON-on-disk registry. It does NOT actually run analysis — +that goes through code-graph's existing `/api/analyze_folder` / +`/api/analyze_repo` endpoints. This module just records what's been done. + +Each entry records (repo, commit, indexed_at, source_path) so the runner +can ask "is `django@4.2.7` already indexed?" before kicking off another +full analysis. Cache is content-addressed by `@`. +""" + +from __future__ import annotations + +import json +import time +from dataclasses import asdict, dataclass +from pathlib import Path + + +@dataclass +class IndexEntry: + repo: str + commit: str + source_path: str + indexed_at: float # unix epoch seconds + + +class IndexCache: + """JSON-file-backed cache. Single-writer assumed (one runner at a time).""" + + def __init__(self, root: str | Path) -> None: + self.root = Path(root) + self.root.mkdir(parents=True, exist_ok=True) + self._file = self.root / "index.json" + + @staticmethod + def key(repo: str, commit: str) -> str: + return f"{repo}@{commit}" + + def _load(self) -> dict[str, IndexEntry]: + if not self._file.exists(): + return {} + with open(self._file) as f: + raw = json.load(f) + return {k: IndexEntry(**v) for k, v in raw.items()} + + def _save(self, entries: dict[str, IndexEntry]) -> None: + data = {k: asdict(v) for k, v in entries.items()} + tmp = self._file.with_suffix(".json.tmp") + with open(tmp, "w") as f: + json.dump(data, f, indent=2, sort_keys=True) + tmp.replace(self._file) + + def has(self, repo: str, commit: str) -> bool: + return self.key(repo, commit) in self._load() + + def get(self, repo: str, commit: str) -> IndexEntry | None: + return self._load().get(self.key(repo, commit)) + + def record(self, repo: str, commit: str, source_path: str) -> IndexEntry: + entries = self._load() + entry = IndexEntry( + repo=repo, + commit=commit, + source_path=str(source_path), + indexed_at=time.time(), + ) + entries[self.key(repo, commit)] = entry + self._save(entries) + return entry + + def forget(self, repo: str, commit: str) -> bool: + entries = self._load() + k = self.key(repo, commit) + if k not in entries: + return False + del entries[k] + self._save(entries) + return True + + def all(self) -> list[IndexEntry]: + return list(self._load().values()) diff --git a/tests/test_bench_index_cache.py b/tests/test_bench_index_cache.py new file mode 100644 index 00000000..ea5e2e55 --- /dev/null +++ b/tests/test_bench_index_cache.py @@ -0,0 +1,58 @@ +"""Tests for bench/runners/index_cache.""" + +from __future__ import annotations + +from pathlib import Path + +from bench.runners.index_cache import IndexCache + + +def test_empty_cache_returns_none(tmp_path: Path): + c = IndexCache(tmp_path) + assert c.has("django", "abc123") is False + assert c.get("django", "abc123") is None + assert c.all() == [] + + +def test_record_and_lookup(tmp_path: Path): + c = IndexCache(tmp_path) + e = c.record("django", "abc123", "/work/django") + assert e.repo == "django" + assert e.source_path == "/work/django" + assert c.has("django", "abc123") is True + got = c.get("django", "abc123") + assert got is not None + assert got.commit == "abc123" + + +def test_persists_across_instances(tmp_path: Path): + c1 = IndexCache(tmp_path) + c1.record("flask", "def456", "/work/flask") + c2 = IndexCache(tmp_path) + assert c2.has("flask", "def456") is True + + +def test_forget(tmp_path: Path): + c = IndexCache(tmp_path) + c.record("sympy", "111", "/work/sympy") + assert c.forget("sympy", "111") is True + assert c.has("sympy", "111") is False + assert c.forget("sympy", "111") is False # second forget is a no-op + + +def test_all_returns_all_entries(tmp_path: Path): + c = IndexCache(tmp_path) + c.record("a", "1", "/p/a") + c.record("b", "2", "/p/b") + c.record("c", "3", "/p/c") + assert {e.repo for e in c.all()} == {"a", "b", "c"} + + +def test_record_overwrites_same_key(tmp_path: Path): + c = IndexCache(tmp_path) + c.record("a", "1", "/p/a") + c.record("a", "1", "/p/a-new") + e = c.get("a", "1") + assert e is not None + assert e.source_path == "/p/a-new" + assert len(c.all()) == 1 From 453dcfa3650b97cef89a684852ee559841be62d8 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 22:19:11 +0300 Subject: [PATCH 16/63] Add LSP adapter (multilspy + jedi) with end-to-end shim tests bench/agents/lsp_adapter.py wraps multilspy's SyncLanguageServer behind the same response shim spec'd in bench/tools/lsp/shim.yaml: cap results at 50, trim hover to 1 signature line + 1 docstring sentence, locations as {path, line, col}. Tools exposed: goto_definition, find_references, hover, document_symbols Notes on the LSP backend choice: - The plan originally specified pyright; multilspy >= 0.0.15 is required for that, but the pinned multilspy fork (AviAvni/multilspy@python-init-params, used by api/analyzers) is older. Using jedi-language-server matches the rest of the repo and avoids a divergent dep tree. Shim normalizes responses so jedi-vs-pyright doesn't affect the validity comparison. - workspace_symbols is dropped: the multilspy fork doesn't implement request_workspace_symbol. Agent falls back to bash+grep, which is the realistic LSP-world fallback too. - MultilspyConfig must be built via from_dict for this fork (constructor doesn't set all fields JediServer expects). Register pytest 'slow' marker in pyproject.toml; the 3 jedi roundtrip tests are slow but currently complete in <4s on a warm cache. Run them with -m slow or default; skip with -m 'not slow'. CONTEXT.md and bench/tools/lsp/tools.yaml updated to match. 10 tests pass: 7 shim units + 3 real jedi roundtrips (goto_definition, hover, document_symbols). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 11 +- bench/agents/lsp_adapter.py | 241 ++++++++++++++++++++++++++++++++ bench/tools/lsp/tools.yaml | 36 +++-- pyproject.toml | 5 + tests/test_bench_lsp_adapter.py | 135 ++++++++++++++++++ 5 files changed, 412 insertions(+), 16 deletions(-) create mode 100644 bench/agents/lsp_adapter.py create mode 100644 tests/test_bench_lsp_adapter.py diff --git a/CONTEXT.md b/CONTEXT.md index 8a7e5718..83b36f72 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -21,9 +21,14 @@ SWE-agent's default file-edit/bash tools (`read_file`, `write_file`, filesystem access is not a useful comparison. ### lsp (config) -`baseline` + multilspy-driven pyright tools (`goto_definition`, -`find_references`, `hover`, `document_symbols`, `workspace_symbols`), -each wrapped by the LSP response shim (see below). +`baseline` + multilspy-driven LSP tools (`goto_definition`, +`find_references`, `hover`, `document_symbols`), each wrapped by the LSP +response shim (see below). The plan originally specified pyright + +`workspace_symbols`; we run **jedi-language-server** (what the pinned +multilspy fork ships) and drop `workspace_symbols` (the fork doesn't +implement `request_workspace_symbol`). The shim normalizes responses +so jedi-vs-pyright does not affect the validity comparison; agent falls +back to bash+grep for workspace-wide symbol search. ### code-graph (config) `baseline` + primitive graph tools: `graph_entities`, `get_neighbors`, diff --git a/bench/agents/lsp_adapter.py b/bench/agents/lsp_adapter.py new file mode 100644 index 00000000..aee8f2e6 --- /dev/null +++ b/bench/agents/lsp_adapter.py @@ -0,0 +1,241 @@ +"""LSP adapter for the `lsp` benchmark config. + +Wraps multilspy's `SyncLanguageServer` so we can expose a small set of +LSP-backed navigation tools to SWE-agent. Every response goes through the +shim defined in `bench/tools/lsp/shim.yaml` so raw LSP verbosity doesn't +dominate the token-cost comparison. + +Notes on the language server choice: +- multilspy 0.0.11 (current pinned version, latest that resolves under our + Python 3.13 constraints) ships **jedi-language-server** for Python, not + pyright. The benchmark validity claim doesn't hinge on jedi-vs-pyright: + the shim normalizes responses to {path, line, col} + 1-line hover, and + both servers are competent Python LSPs. If we later need pyright, we'd + bump multilspy to ≥0.0.15. +- multilspy 0.0.11 also has no `request_workspace_symbol`. We therefore + drop `workspace_symbols` from the LSP tool set; the agent falls back to + bash+grep, which is what real LSP workflows actually do. +""" + +from __future__ import annotations + +import logging +import os +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterator + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Shim configuration — mirrors bench/tools/lsp/shim.yaml +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class LSPShim: + max_results_per_call: int = 50 + hover_signature_lines: int = 1 + hover_docstring_sentences: int = 1 + + +DEFAULT_SHIM = LSPShim() + + +def _trim_hover(contents: Any, shim: LSPShim) -> str: + """Reduce an LSP Hover contents blob to 1 signature + 1 docstring sentence.""" + text = _hover_to_str(contents) + if not text: + return "" + # Drop fence-only lines and empty lines, then split into signature vs rest. + real_lines = [ + ln for ln in text.splitlines() + if ln.strip() and not ln.strip().startswith("```") + ] + signature_part = real_lines[: shim.hover_signature_lines] + rest_lines = real_lines[shim.hover_signature_lines :] + docstring = " ".join(ln.lstrip("> ").strip() for ln in rest_lines) + sentences = _split_sentences(docstring, shim.hover_docstring_sentences) + + parts: list[str] = [] + parts.extend(signature_part) + if sentences: + parts.append(sentences) + return "\n".join(parts).strip() + + +def _hover_to_str(contents: Any) -> str: + if contents is None: + return "" + if isinstance(contents, str): + return contents + if isinstance(contents, dict): + return str(contents.get("value", "")) + if isinstance(contents, list): + return "\n".join(_hover_to_str(c) for c in contents) + return str(contents) + + +def _split_sentences(text: str, n: int) -> str: + text = text.strip() + if not text or n <= 0: + return "" + # Lightweight sentence split — good enough for docstrings. + parts: list[str] = [] + buf: list[str] = [] + for ch in text: + buf.append(ch) + if ch in ".!?": + parts.append("".join(buf).strip()) + buf = [] + if len(parts) >= n: + break + if not parts and buf: + parts.append("".join(buf).strip()) + return " ".join(parts[:n]) + + +def _location_to_dict(loc: dict[str, Any]) -> dict[str, Any]: + """Convert a multilspy Location (TypedDict) to the shimmed shape.""" + rng = loc.get("range") or {} + start = rng.get("start") or {} + path = loc.get("relativePath") or loc.get("absolutePath") or "" + return { + "path": path, + "line": int(start.get("line", 0)), + "col": int(start.get("character", 0)), + } + + +def _cap(items: list[Any], shim: LSPShim) -> list[Any]: + return items[: shim.max_results_per_call] + + +# --------------------------------------------------------------------------- +# Client +# --------------------------------------------------------------------------- + +class LSPClient: + """Lazily-started multilspy session for a single repo root. + + Use as a context manager; the underlying language server subprocess + lives only inside `with server_running()`. + """ + + def __init__(self, repo_root: str | Path, language: str = "python", + shim: LSPShim = DEFAULT_SHIM, + environment_path: str | None = None) -> None: + self.repo_root = str(Path(repo_root).resolve()) + self.language = language + self.shim = shim + self._env_path = environment_path + self._server: Any | None = None # SyncLanguageServer + + # ----- lifecycle ------------------------------------------------------ + + def _build_server(self) -> Any: + # Local imports keep this module importable without multilspy installed. + from multilspy import SyncLanguageServer + from multilspy.multilspy_config import MultilspyConfig + from multilspy.multilspy_logger import MultilspyLogger + + # NOTE: The multilspy fork we depend on expects MultilspyConfig built + # via `from_dict` (the constructor doesn't initialize all fields the + # JediServer reads). For Python we also need an environment_path so + # jedi knows where to look for installed packages — fall back to the + # current interpreter's prefix if the caller doesn't provide one. + import sys as _sys + config_dict: dict[str, Any] = {"code_language": self.language} + if self.language == "python": + config_dict["environment_path"] = self._env_path or _sys.prefix + else: + # Pass through if caller explicitly set an env path. + if self._env_path: + config_dict["environment_path"] = self._env_path + config = MultilspyConfig.from_dict(config_dict) + logger_ = MultilspyLogger() + return SyncLanguageServer.create(config, logger_, self.repo_root) + + @contextmanager + def server_running(self) -> Iterator["LSPClient"]: + self._server = self._build_server() + with self._server.start_server(): + try: + yield self + finally: + self._server = None + + # ----- relative path normalization ----------------------------------- + + def _rel(self, file_path: str) -> str: + p = Path(file_path) + if p.is_absolute(): + try: + return str(p.relative_to(self.repo_root)) + except ValueError: + return str(p) + return file_path + + # ----- tools (names mirror bench/tools/lsp/tools.yaml) --------------- + + def goto_definition(self, file: str, line: int, col: int) -> list[dict[str, Any]]: + assert self._server is not None, "LSP server not started" + raw = self._server.request_definition(self._rel(file), line, col) or [] + return _cap([_location_to_dict(loc) for loc in raw], self.shim) + + def find_references(self, file: str, line: int, col: int) -> list[dict[str, Any]]: + assert self._server is not None, "LSP server not started" + raw = self._server.request_references(self._rel(file), line, col) or [] + return _cap([_location_to_dict(loc) for loc in raw], self.shim) + + def hover(self, file: str, line: int, col: int) -> dict[str, Any]: + assert self._server is not None, "LSP server not started" + raw = self._server.request_hover(self._rel(file), line, col) + if not raw: + return {"text": ""} + return {"text": _trim_hover(raw.get("contents"), self.shim)} + + def document_symbols(self, file: str) -> list[dict[str, Any]]: + assert self._server is not None, "LSP server not started" + raw = self._server.request_document_symbols(self._rel(file)) + # multilspy returns (symbols, tree). We only want the flat list. + symbols = raw[0] if isinstance(raw, tuple) else (raw or []) + out: list[dict[str, Any]] = [] + for s in symbols: + loc = s.get("location") or {} + d = _location_to_dict(loc) if loc else {"path": self._rel(file), "line": 0, "col": 0} + d["name"] = s.get("name", "") + d["kind"] = s.get("kind", "") + out.append(d) + return _cap(out, self.shim) + + +# --------------------------------------------------------------------------- +# Module-level callables — what SWE-agent tool registries expect +# --------------------------------------------------------------------------- + +def _client_from_env(shim: LSPShim = DEFAULT_SHIM) -> LSPClient: + repo_root = os.environ.get("LSP_REPO_ROOT") or os.getcwd() + language = os.environ.get("LSP_LANGUAGE", "python") + return LSPClient(repo_root=repo_root, language=language, shim=shim) + + +def goto_definition(file: str, line: int, col: int) -> list[dict[str, Any]]: + with _client_from_env().server_running() as c: + return c.goto_definition(file, line, col) + + +def find_references(file: str, line: int, col: int) -> list[dict[str, Any]]: + with _client_from_env().server_running() as c: + return c.find_references(file, line, col) + + +def hover(file: str, line: int, col: int) -> dict[str, Any]: + with _client_from_env().server_running() as c: + return c.hover(file, line, col) + + +def document_symbols(file: str) -> list[dict[str, Any]]: + with _client_from_env().server_running() as c: + return c.document_symbols(file) diff --git a/bench/tools/lsp/tools.yaml b/bench/tools/lsp/tools.yaml index cf59ec58..f9f11a97 100644 --- a/bench/tools/lsp/tools.yaml +++ b/bench/tools/lsp/tools.yaml @@ -1,22 +1,32 @@ # SWE-agent tool bundle: lsp config. # -# Baseline + pyright-backed navigation via multilspy. The benchmark -# instantiates one multilspy LanguageServer per task (cached per -# @) and exposes the five tools below as SWE-agent custom -# commands that shell out to a thin Python adapter. +# Baseline + LSP-backed navigation via multilspy. The benchmark +# instantiates one multilspy SyncLanguageServer per task (cached per +# @) and exposes the four tools below as SWE-agent custom +# commands that shell out to bench/agents/lsp_adapter.py. extends: ../baseline/tools.yaml tools: - - goto_definition # (file, line, col) -> [definition_location] - - find_references # (file, line, col) -> [reference_location] - - hover # (file, line, col) -> markdown - - document_symbols # (file) -> [symbol] - - workspace_symbols # (query) -> [symbol] + - goto_definition # (file, line, col) -> [{path, line, col}] + - find_references # (file, line, col) -> [{path, line, col}] + - hover # (file, line, col) -> {text} + - document_symbols # (file) -> [{name, kind, path, line, col}] + +# workspace_symbols is intentionally NOT exposed: the multilspy fork we +# depend on (forked at python-init-params) does not implement +# request_workspace_symbol. The agent falls back to bash+grep, which is +# also what real-world LSP workflows do. backend: - language_server: pyright + language_server: jedi-language-server driver: multilspy - - # Multilspy starts pyright as a subprocess per repo; we cache the - # server handle in bench/cache/lsp/@. + # Notes: + # - The plan originally specified pyright. multilspy ≥0.0.15 is required + # for that, but our pinned fork is older (it adds environment_path + # support for jedi to multilspy 0.0.11). The shim normalizes responses + # so jedi-vs-pyright doesn't affect the validity comparison. + # - environment_path is passed through MultilspyConfig.from_dict; + # defaults to the current interpreter's sys.prefix for jedi. + # + # Cache: bench/cache/lsp/@ tracks first-run setup time. diff --git a/pyproject.toml b/pyproject.toml index a2d5f7a9..3303a1a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,5 +29,10 @@ test = [ "httpx>=0.28.0,<1.0.0", ] +[tool.pytest.ini_options] +markers = [ + "slow: marks tests that spawn external subprocesses (LSP servers, FalkorDB, etc.); skip with -m 'not slow'", +] + [tool.setuptools.packages.find] where = ["."] diff --git a/tests/test_bench_lsp_adapter.py b/tests/test_bench_lsp_adapter.py new file mode 100644 index 00000000..4b55d864 --- /dev/null +++ b/tests/test_bench_lsp_adapter.py @@ -0,0 +1,135 @@ +"""Tests for the LSP adapter — shim logic + a real jedi roundtrip. + +The shim helpers are pure functions tested directly. The actual LSP +roundtrip test starts jedi-language-server against a tiny in-tree fixture +and asserts goto_definition + hover behave end-to-end. That test is +marked `slow` so CI can skip it when LSP installs are unavailable. +""" + +from __future__ import annotations + +import textwrap +from pathlib import Path + +import pytest + +from bench.agents.lsp_adapter import ( + DEFAULT_SHIM, + LSPClient, + LSPShim, + _cap, + _location_to_dict, + _trim_hover, +) + + +# --------------------------------------------------------------------------- +# Shim unit tests (no LSP server needed) +# --------------------------------------------------------------------------- + +def test_trim_hover_keeps_one_signature_and_one_sentence(): + raw = textwrap.dedent(""" + def greet(name: str) -> str + Return a greeting for the given name. This part is extra. Even more text. + """).strip() + out = _trim_hover(raw, DEFAULT_SHIM) + assert "def greet" in out + assert "Return a greeting for the given name." in out + assert "extra" not in out + + +def test_trim_hover_strips_code_fences(): + raw = "```python\ndef foo()\n```\nShort docstring." + out = _trim_hover(raw, DEFAULT_SHIM) + assert "```" not in out + assert "def foo()" in out + + +def test_trim_hover_handles_dict_and_list_contents(): + assert "x" in _trim_hover({"value": "x"}, DEFAULT_SHIM) + assert "a" in _trim_hover([{"value": "a"}, "b"], DEFAULT_SHIM) + assert _trim_hover(None, DEFAULT_SHIM) == "" + + +def test_location_to_dict_uses_relative_path(): + loc = { + "relativePath": "src/a.py", + "absolutePath": "/abs/src/a.py", + "range": {"start": {"line": 10, "character": 4}, "end": {"line": 10, "character": 8}}, + } + assert _location_to_dict(loc) == {"path": "src/a.py", "line": 10, "col": 4} + + +def test_location_to_dict_falls_back_to_absolute_when_no_relative(): + loc = {"absolutePath": "/abs/x.py", "range": {"start": {"line": 0, "character": 0}}} + assert _location_to_dict(loc)["path"] == "/abs/x.py" + + +def test_cap_limits_results(): + items = list(range(200)) + assert len(_cap(items, LSPShim(max_results_per_call=10))) == 10 + + +def test_shim_dataclass_defaults_match_yaml(): + # If you change shim.yaml, change DEFAULT_SHIM too. + assert DEFAULT_SHIM.max_results_per_call == 50 + assert DEFAULT_SHIM.hover_signature_lines == 1 + assert DEFAULT_SHIM.hover_docstring_sentences == 1 + + +# --------------------------------------------------------------------------- +# LSP roundtrip — slow, runs real jedi subprocess +# --------------------------------------------------------------------------- + + +@pytest.fixture +def python_repo(tmp_path: Path) -> Path: + (tmp_path / "sample.py").write_text(textwrap.dedent('''\ + def greet(name): + """Return a greeting for the given name.""" + return f"hello {name}" + + + def main(): + who = "world" + return greet(who) + ''') + ) + return tmp_path + + +@pytest.mark.slow +def test_real_jedi_goto_definition_finds_greet(python_repo: Path): + """End-to-end against jedi-language-server. + + Skipped on `pytest -m 'not slow'`. Heavy: spawns the LSP subprocess + and downloads jedi on first run. + """ + client = LSPClient(repo_root=python_repo, language="python") + with client.server_running(): + # In sample.py, `greet(who)` is called on line 7 (0-indexed). The + # call site sits at "return greet(who)" — `greet` starts around col 11. + defs = client.goto_definition("sample.py", line=7, col=11) + # We expect at least one definition pointing back to sample.py line 0 + # (where `def greet` lives). + assert any(d["path"].endswith("sample.py") and d["line"] == 0 for d in defs), defs + + +@pytest.mark.slow +def test_real_jedi_hover_is_shimmed(python_repo: Path): + client = LSPClient(repo_root=python_repo, language="python") + with client.server_running(): + h = client.hover("sample.py", line=7, col=11) + text = h["text"] + assert "greet" in text or text == "" # jedi can sometimes return empty hover + # No more than ~3 lines of output after shimming (signature + 1 sentence). + assert text.count("\n") <= 3 + + +@pytest.mark.slow +def test_real_jedi_document_symbols_lists_top_level_funcs(python_repo: Path): + client = LSPClient(repo_root=python_repo, language="python") + with client.server_running(): + syms = client.document_symbols("sample.py") + names = {s["name"] for s in syms} + assert {"greet", "main"}.issubset(names) From c6ef736a56bc359ee8918170dcb4d5da76a2bb56 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 22:28:58 +0300 Subject: [PATCH 17/63] Add mini-swe-agent benchmark runner with dry-run mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pivots the harness from SWE-agent to mini-swe-agent — upstream now recommends mini-, and its bash-only tool surface is a simpler integration: each config is a PATH prefix plus a system_preamble.md, not a per-config tools.yaml. What this adds: - bench/runners/mini_runner.py — wraps DefaultAgent + LocalEnvironment, per-config env wiring (PATH for lsp/code_graph, baseline untouched), trajectory + diff capture, JSONL append via bench.metrics. Includes a stub LLM model that exercises the entire loop without any network calls so the harness is testable today. - bench/cli/cg.py, bench/cli/lsp.py — bash-callable CLIs wrapping the existing CodeGraphClient and LSP adapter. These are what the agent invokes via bash. - bench/tools/{baseline,lsp,code_graph}/system_preamble.md — symmetric one-page preambles per the locked-in grill decision. - bench/metrics — extended to also parse mini-swe-agent trajectory shape (messages[*].extra.response.usage and extra.actions[*].command). Buckets bash commands by first token; the COMPLETE_TASK submit protocol is bucketed as 'submit'. - tests/test_bench_runner.py — 10 tests, all run offline (no LLM): smoke, env wiring, persistence, CLI argparse smoke. - CONTEXT.md + plan.md — reflect mini-swe-agent + jedi pivots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 69 +- bench/cli/__init__.py | 0 bench/cli/cg.py | 89 +++ bench/cli/lsp.py | 78 ++ bench/metrics/__init__.py | 56 +- bench/runners/mini_runner.py | 433 +++++++++++ bench/tools/baseline/system_preamble.md | 20 + bench/tools/code_graph/system_preamble.md | 39 + bench/tools/lsp/system_preamble.md | 33 + pyproject.toml | 3 + tests/test_bench_runner.py | 150 ++++ uv.lock | 866 +++++++++++++++++++++- 12 files changed, 1798 insertions(+), 38 deletions(-) create mode 100644 bench/cli/__init__.py create mode 100644 bench/cli/cg.py create mode 100644 bench/cli/lsp.py create mode 100644 bench/runners/mini_runner.py create mode 100644 bench/tools/baseline/system_preamble.md create mode 100644 bench/tools/code_graph/system_preamble.md create mode 100644 bench/tools/lsp/system_preamble.md create mode 100644 tests/test_bench_runner.py diff --git a/CONTEXT.md b/CONTEXT.md index 83b36f72..072acea8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -7,34 +7,42 @@ changes). Not a project-wide glossary for code-graph. ### Agent The autonomous loop that reads a task, calls tools, edits code, and -submits a result. We adopt **SWE-agent** (Princeton) as the harness; we -do not write our own. The agent loop is fixed across all configs. +submits a result. We adopt **mini-swe-agent** (SWE-agent project's +recommended minimal harness) as the agent. The original SWE-agent +is **not** used: upstream now points users at mini- instead, and its +bash-only tool surface is a much smaller, more transparent +integration. The agent loop is fixed across all configs. ### Config One of `baseline`, `lsp`, `code-graph`. A config is **fully defined by -its `tools.yaml` plus a one-paragraph preamble**. Same model, same -scaffolding prompt, same iteration cap across all three. +its `system_preamble.md` plus the `PATH` it exposes to the agent's +bash**. Same model, same scaffolding template, same step/cost limits +across all three. (mini-swe-agent has no per-config `tools.yaml` +because bash is the only tool; the per-config `tools.yaml` files in +the repo are kept as design documentation.) ### baseline (config) -SWE-agent's default file-edit/bash tools (`read_file`, `write_file`, -`edit`, `bash`, `submit`). **Not "zero tools"** — an LLM with no -filesystem access is not a useful comparison. +mini-swe-agent's stock bash environment — `cat`, `grep`, `find`, +`sed`, `git`, the agent's own implicit submit protocol. **Not +"zero tools"** — an LLM with no filesystem access is not a useful +comparison. ### lsp (config) -`baseline` + multilspy-driven LSP tools (`goto_definition`, -`find_references`, `hover`, `document_symbols`), each wrapped by the LSP -response shim (see below). The plan originally specified pyright + -`workspace_symbols`; we run **jedi-language-server** (what the pinned -multilspy fork ships) and drop `workspace_symbols` (the fork doesn't -implement `request_workspace_symbol`). The shim normalizes responses -so jedi-vs-pyright does not affect the validity comparison; agent falls -back to bash+grep for workspace-wide symbol search. +`baseline` + an `lsp` command on PATH that wraps multilspy/jedi +(`goto-definition`, `find-references`, `hover`, `document-symbols`), +each shaped by the LSP response shim (see below). The plan originally +specified pyright + `workspace_symbols`; we run **jedi-language-server** +(what the pinned multilspy fork ships) and drop `workspace_symbols` +(the fork doesn't implement `request_workspace_symbol`). The shim +normalizes responses so jedi-vs-pyright does not affect the validity +comparison; agent falls back to bash+grep for workspace-wide symbol +search. ### code-graph (config) -`baseline` + primitive graph tools: `graph_entities`, `get_neighbors`, -`find_paths`, `auto_complete`, `find_symbol`, plus `note_edit`. The -GraphRAG `chat` endpoint is **excluded** to avoid nested-agent token -double-counting. +`baseline` + a `cg` command on PATH that talks to the code-graph +HTTP service: `graph-entities`, `get-neighbors`, `find-paths`, +`auto-complete`, `find-symbol`, plus `note-edit`. The GraphRAG `chat` +endpoint is **excluded** to avoid nested-agent token double-counting. ### Accuracy The SWE-bench end-to-end metric: did the agent's patch pass the repo's @@ -65,15 +73,17 @@ A `@` for which a FalkorDB graph has been built. Cache key. No incremental indexing across commits. ### Tool service architecture -SWE-agent runs in a Docker container per task. **Tools live in that -container** (Option C from the grill): multilspy/pyright runs -in-process there; code-graph is reached via an HTTP client to a -host-side FastAPI + FalkorDB. The repo is bind-mounted; the agent's -edits are visible to pyright immediately. code-graph's graph is built -once per `@` and would otherwise go stale on agent edits, -so the code-graph bundle includes a `note_edit(path)` tool that -triggers a **single-file incremental re-index** of the touched file. -This keeps fairness with the live-by-default LSP. +mini-swe-agent runs each step as `subprocess.run` in a configured cwd +(the prepared repo working tree). **Tools live on the host** (local +process model): multilspy/jedi runs in-process via the `lsp` CLI +wrapper; code-graph is reached via an HTTP client (`cg` CLI wrapper) +to the FastAPI + FalkorDB service. The runner sets `PATH` so the +agent sees `bench/cli/` only for configs that include those tools +(baseline gets the unmodified host `PATH`). code-graph's graph is +built once per `@` and would otherwise go stale on agent +edits, so the code-graph bundle includes a `cg note-edit PATH` tool +that triggers a **single-file incremental re-index** of the touched +file. This keeps fairness with the live-by-default LSP. ### LSP response shim Raw LSP responses are too verbose for a fair token-cost comparison @@ -120,6 +130,7 @@ before publishing — the 50-task sample's confidence interval is roughly - `bench/` is the top-level directory for the workstream. - Results are JSONL, one row per `(task_id, config, run_idx)`, with - token counts pulled from the SWE-agent trajectory JSON. + token counts pulled from the mini-swe-agent trajectory JSON + (`agent.serialize()` — `messages[*].extra.response.usage`). - The opencode track and RepoBench track are **not** part of this workstream (dropped during the grill). diff --git a/bench/cli/__init__.py b/bench/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bench/cli/cg.py b/bench/cli/cg.py new file mode 100644 index 00000000..4b74be0e --- /dev/null +++ b/bench/cli/cg.py @@ -0,0 +1,89 @@ +"""`cg` — bash-callable CLI exposing code-graph primitives. + +mini-swe-agent only uses bash, so each "tool" we want the agent to have is +just a CLI it can invoke. This script wraps bench/agents/code_graph_adapter +behind a small argparse interface and prints JSON results to stdout, one +JSON document per call. + +Usage examples (run inside the agent's bash environment): + + cg graph-entities --repo django + cg get-neighbors --repo django --ids 12 14 17 + cg find-paths --repo django --src 12 --dst 88 + cg auto-complete --repo django --prefix get_user + cg find-symbol --repo django --name get_user_model + cg note-edit --repo django --path src/django/contrib/auth/models.py + +Required env vars (set by the runner): + CODEGRAPH_URL base URL of the code-graph service + SECRET_TOKEN bearer token (omit if the server has CODE_GRAPH_PUBLIC=1) +""" + +from __future__ import annotations + +import argparse +import json +import sys + +from bench.agents.code_graph_adapter import CodeGraphClient + + +def _print(obj: object) -> None: + json.dump(obj, sys.stdout, indent=2, sort_keys=True) + sys.stdout.write("\n") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="cg", description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + def add_repo(p: argparse.ArgumentParser) -> None: + p.add_argument("--repo", required=True, help="repository name in the graph") + + ge = sub.add_parser("graph-entities") + add_repo(ge) + + gn = sub.add_parser("get-neighbors") + add_repo(gn) + gn.add_argument("--ids", type=int, nargs="+", required=True) + + fp = sub.add_parser("find-paths") + add_repo(fp) + fp.add_argument("--src", type=int, required=True) + fp.add_argument("--dst", type=int, required=True) + + ac = sub.add_parser("auto-complete") + add_repo(ac) + ac.add_argument("--prefix", required=True) + + fs = sub.add_parser("find-symbol") + add_repo(fs) + fs.add_argument("--name", required=True) + + ne = sub.add_parser("note-edit") + add_repo(ne) + ne.add_argument("--path", required=True) + + args = parser.parse_args(argv) + + with CodeGraphClient() as c: + if args.cmd == "graph-entities": + _print(c.graph_entities(args.repo)) + elif args.cmd == "get-neighbors": + _print(c.get_neighbors(args.repo, args.ids)) + elif args.cmd == "find-paths": + _print(c.find_paths(args.repo, args.src, args.dst)) + elif args.cmd == "auto-complete": + _print(c.auto_complete(args.repo, args.prefix)) + elif args.cmd == "find-symbol": + _print(c.find_symbol(args.repo, args.name)) + elif args.cmd == "note-edit": + _print(c.note_edit(args.repo, args.path)) + else: + parser.error(f"unknown command {args.cmd}") + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/cli/lsp.py b/bench/cli/lsp.py new file mode 100644 index 00000000..2be91202 --- /dev/null +++ b/bench/cli/lsp.py @@ -0,0 +1,78 @@ +"""`lsp` — bash-callable CLI exposing LSP primitives. + +mini-swe-agent only uses bash, so we surface multilspy/jedi as a small +CLI the agent can invoke. Wraps bench/agents/lsp_adapter. + +Usage examples (run inside the agent's bash environment): + + lsp goto-definition --file path/to/x.py --line 12 --col 4 + lsp find-references --file path/to/x.py --line 12 --col 4 + lsp hover --file path/to/x.py --line 12 --col 4 + lsp document-symbols --file path/to/x.py + +Required env vars (set by the runner): + LSP_REPO_ROOT absolute path to the repo being analyzed + LSP_LANGUAGE optional; defaults to "python" + +Startup cost: each invocation spawns its own LSP subprocess (~1-3s +warm). For tasks where the agent makes many LSP calls, the runner can +keep a long-lived server by setting LSP_SERVER_FIFO; that's a future +optimization — for now we accept the per-call cost in exchange for the +simpler subprocess.run model mini-swe-agent uses. +""" + +from __future__ import annotations + +import argparse +import json +import sys + +from bench.agents.lsp_adapter import _client_from_env + + +def _print(obj: object) -> None: + json.dump(obj, sys.stdout, indent=2, sort_keys=True) + sys.stdout.write("\n") + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="lsp", description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + def add_pos(p: argparse.ArgumentParser) -> None: + p.add_argument("--file", required=True) + p.add_argument("--line", type=int, required=True) + p.add_argument("--col", type=int, required=True) + + gd = sub.add_parser("goto-definition") + add_pos(gd) + + fr = sub.add_parser("find-references") + add_pos(fr) + + hv = sub.add_parser("hover") + add_pos(hv) + + ds = sub.add_parser("document-symbols") + ds.add_argument("--file", required=True) + + args = parser.parse_args(argv) + + client = _client_from_env() + with client.server_running() as c: + if args.cmd == "goto-definition": + _print(c.goto_definition(args.file, args.line, args.col)) + elif args.cmd == "find-references": + _print(c.find_references(args.file, args.line, args.col)) + elif args.cmd == "hover": + _print(c.hover(args.file, args.line, args.col)) + elif args.cmd == "document-symbols": + _print(c.document_symbols(args.file)) + else: + parser.error(f"unknown command {args.cmd}") + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/metrics/__init__.py b/bench/metrics/__init__.py index 22788381..4427b55f 100644 --- a/bench/metrics/__init__.py +++ b/bench/metrics/__init__.py @@ -63,15 +63,38 @@ def _first_int(d: dict[str, Any], keys: tuple[str, ...]) -> int: def _iter_history_steps(traj: dict[str, Any]) -> list[dict[str, Any]]: """SWE-agent has flipped between top-level `history`, `trajectory`, and - `steps`. Return whichever list is present, else []. + `steps`. mini-swe-agent uses `messages`. Return whichever list is + present, else []. """ - for key in ("history", "trajectory", "steps"): + for key in ("history", "trajectory", "steps", "messages"): v = traj.get(key) if isinstance(v, list): return v return [] +def _step_usage(step: dict[str, Any]) -> dict[str, Any] | None: + """Find an OpenAI/Anthropic-style usage dict on a step, across schemas.""" + if not isinstance(step, dict): + return None + # SWE-agent: step.usage + usage = step.get("usage") + if isinstance(usage, dict): + return usage + # mini-swe-agent: step.extra.response.usage + extra = step.get("extra") + if isinstance(extra, dict): + resp = extra.get("response") + if isinstance(resp, dict): + u = resp.get("usage") + if isinstance(u, dict): + return u + u = extra.get("usage") + if isinstance(u, dict): + return u + return None + + def extract_token_usage(traj: dict[str, Any]) -> tuple[int, int]: """Sum input + output tokens across all LLM calls in the trajectory. @@ -81,11 +104,10 @@ def extract_token_usage(traj: dict[str, Any]) -> tuple[int, int]: total_in = 0 total_out = 0 for step in _iter_history_steps(traj): - usage = step.get("usage") if isinstance(step, dict) else None + usage = _step_usage(step) if isinstance(usage, dict): total_in += _first_int(usage, _TOKEN_KEYS_IN) total_out += _first_int(usage, _TOKEN_KEYS_OUT) - # Fall back to a top-level summary if present. summary = traj.get("total_usage") or traj.get("usage") if (total_in == 0 and total_out == 0) and isinstance(summary, dict): total_in = _first_int(summary, _TOKEN_KEYS_IN) @@ -93,6 +115,23 @@ def extract_token_usage(traj: dict[str, Any]) -> tuple[int, int]: return total_in, total_out +def _action_name(cmd: str) -> str: + """Bucket a bash command line into a friendly tool-call name. + + The first non-redirection token is good enough — `cg`, `lsp`, `git`, + `grep`, `sed`, etc. Falls back to `bash` for empty or odd shapes. + """ + if not isinstance(cmd, str): + return "bash" + tokens = cmd.strip().split() + if not tokens: + return "bash" + head = tokens[0] + if head in ("printf", "echo") and "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" in cmd: + return "submit" + return head + + def extract_tool_calls(traj: dict[str, Any]) -> tuple[int, dict[str, int]]: """Count every tool/action invocation and bucket by tool name.""" by_name: dict[str, int] = {} @@ -100,6 +139,15 @@ def extract_tool_calls(traj: dict[str, Any]) -> tuple[int, dict[str, int]]: for step in _iter_history_steps(traj): if not isinstance(step, dict): continue + # mini-swe-agent: step.extra.actions[*].command (bash-only). + extra = step.get("extra") if isinstance(step.get("extra"), dict) else None + if extra and isinstance(extra.get("actions"), list): + for act in extra["actions"]: + if isinstance(act, dict): + by_name_key = _action_name(act.get("command", "")) + by_name[by_name_key] = by_name.get(by_name_key, 0) + 1 + total += 1 + continue # SWE-agent shapes: action.command, tool_calls[*].function.name, action.name name = None action = step.get("action") diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py new file mode 100644 index 00000000..6eb74cf0 --- /dev/null +++ b/bench/runners/mini_runner.py @@ -0,0 +1,433 @@ +"""Benchmark runner wired to mini-swe-agent. + +The runner is the glue between: + +- the **benchmark dataset** (SWE-bench Verified, or a tiny synthetic + dataset in `--dry-run` mode so the harness is testable without an + LLM API key), +- the **agent harness** (`mini-swe-agent`, which uses bash as its sole + tool surface), +- and the **per-config tool bundle** (baseline / lsp / code-graph), + exposed to the agent as a `PATH` prefix containing `bench/cli/` + scripts plus the relevant env vars. + +For each (task, config) pair the runner: + +1. Materializes the target repo at the task's base commit. +2. Builds the agent: LitellmModel + LocalEnvironment (cwd = repo, env = + config-specific tool env vars) + system/instance templates assembled + from `bench/tools//system_preamble.md` and the task body. +3. Runs the agent with the locked-in step/cost/wall-time limits. +4. Captures the trajectory (mini-swe-agent's `agent.serialize()` dict) + and `git diff` of the repo as the proposed patch. +5. Hands the trajectory to `bench.metrics.task_metrics_from_trajectory` + and appends a row to `bench/cache/results.jsonl`. + +The `--dry-run` mode swaps the LLM model for a deterministic stub so +the entire pipeline can run with no Anthropic key — this is the mode +we exercise in CI and in `tests/test_bench_runner.py`. +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +# mini-swe-agent imports are slow (litellm pre-import). Defer to call time. + +REPO_ROOT = Path(__file__).resolve().parents[2] +BENCH_DIR = REPO_ROOT / "bench" +CLI_DIR = BENCH_DIR / "cli" +TOOLS_DIR = BENCH_DIR / "tools" +DEFAULT_CACHE_DIR = BENCH_DIR / "cache" +DEFAULT_RESULTS = DEFAULT_CACHE_DIR / "results.jsonl" + +VALID_CONFIGS = ("baseline", "lsp", "code_graph") + + +# --------------------------------------------------------------------------- +# Task model +# --------------------------------------------------------------------------- + + +@dataclass(slots=True) +class Task: + """A single benchmark instance. + + `repo_path` is an absolute path to the prepared working tree at the + correct base commit. `verify_cmd` is a shell command that must exit + 0 if the patch resolves the task; in dry-run mode it's the cheap + synthetic check, in real SWE-bench mode it's the harness's test + selection. + """ + + task_id: str + repo_name: str + repo_path: Path + problem_statement: str + verify_cmd: str | None = None + + +# --------------------------------------------------------------------------- +# Preamble + instance prompt assembly +# --------------------------------------------------------------------------- + + +# The instance prompt is identical across configs — only `system_preamble.md` +# changes. mini-swe-agent uses Jinja2 templating, so we pre-format with +# explicit placeholders the agent will see. +INSTANCE_TEMPLATE = """\ +You are working in the repository at {{cwd}}. + +The task to solve: + +{{task}} + +When you believe the task is complete, finish your turn with a final +message that contains a unified diff of your changes inside a fenced +``` block, then exit. Do not commit; the harness reads the diff via +`git diff`. +""" + + +def load_preamble(config: str) -> str: + """Read the per-config system preamble; fall back to a generic stub.""" + path = TOOLS_DIR / config / "system_preamble.md" + if path.exists(): + return path.read_text() + # Fallback so dry-run tests can run before all preambles are authored. + return ( + f"You are an autonomous coding agent. Configuration: {config}.\n" + "You have one tool: bash. Run commands to read files, search, and " + "edit. Available helpers depend on the configuration.\n" + ) + + +# --------------------------------------------------------------------------- +# Per-config environment +# --------------------------------------------------------------------------- + + +def config_env(config: str, repo_path: Path) -> dict[str, str]: + """Build the environment variables the agent's bash sees. + + The key trick: each config prepends `bench/cli` to PATH only when + the helper scripts are part of the bundle. For the `baseline` + config we deliberately do NOT add the helpers — that's the whole + point of the baseline. + """ + env = dict(os.environ) + # Don't leak the user's PATH editor / shell aliases into the agent. + env["PATH"] = ( + f"{CLI_DIR}:{env.get('PATH', '/usr/bin:/bin')}" + if config != "baseline" + else env.get("PATH", "/usr/bin:/bin") + ) + # Make `python -m bench.cli.cg ...` work too, regardless of cwd. + env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + if config == "lsp": + env["LSP_REPO_ROOT"] = str(repo_path) + env.setdefault("LSP_LANGUAGE", "python") + elif config == "code_graph": + # The runner is responsible for ensuring the service is up. + env.setdefault("CODEGRAPH_URL", "http://127.0.0.1:5000") + return env + + +# --------------------------------------------------------------------------- +# Dry-run stub model +# --------------------------------------------------------------------------- + + +class _DryRunModel: + """A stand-in for a real LLM. Exercises mini-swe-agent's full loop + without making any network calls. + + Returns a single fake "tool call" that submits the task immediately + using mini-swe-agent's `COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT` + bash protocol. The bash command itself echoes a tiny payload that + proves the per-config env is wired correctly (e.g. PATH for lsp / + code-graph configs contains `bench/cli/`). + + This is the v2 tool-calls shape: `extra.actions` is a list of dicts + with a `"command"` key, which is what `LocalEnvironment.execute` + consumes. + """ + + def __init__(self, *, marker: str = "dry-run-ok") -> None: + self.n_calls = 0 + self.cost = 0.0 + self.config = type("cfg", (), { + "model_name": "dry-run-stub", + "model_dump": lambda self_, **_: {"model_name": "dry-run-stub"}, + "multimodal_regex": "", + })() + self._marker = marker + + def query(self, messages: list[dict[str, Any]], **_: Any) -> dict[str, Any]: + self.n_calls += 1 + usage = {"prompt_tokens": 10 * self.n_calls, + "completion_tokens": 5, + "total_tokens": 10 * self.n_calls + 5} + # Bash payload: first line of stdout = sentinel; subsequent lines + # = final answer. The sentinel makes LocalEnvironment raise + # Submitted, which the agent treats as a clean exit. + cmd = ( + "printf 'COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\\n%s\\n' " + f"'{self._marker}: PATH=$PATH'" + ) + return { + "role": "assistant", + "content": f"Submitting ({self._marker}).", + "extra": { + "actions": [{"command": cmd}], + "cost": 0.0, + "response": {"usage": usage}, + }, + } + + # The agent calls these helpers on the model; provide minimal stubs. + def format_message(self, **kwargs: Any) -> dict[str, Any]: + return dict(kwargs) + + def format_observation_messages( + self, message: dict[str, Any], outputs: list[dict[str, Any]], + template_vars: dict[str, Any] | None = None, + ) -> list[dict[str, Any]]: + return [{"role": "user", "content": str(o.get("output", ""))} for o in outputs] + + def get_template_vars(self, **_: Any) -> dict[str, Any]: + return {"model_name": "dry-run-stub"} + + def serialize(self) -> dict[str, Any]: + return {"info": {"config": {"model": {"model_name": "dry-run-stub"}, + "model_type": "dry-run-stub"}}} + + +# --------------------------------------------------------------------------- +# Single-task execution +# --------------------------------------------------------------------------- + + +def _capture_diff(repo_path: Path) -> str: + try: + out = subprocess.run( + ["git", "diff"], cwd=repo_path, capture_output=True, text=True, check=False, + ) + return out.stdout + except FileNotFoundError: + return "" + + +def run_task( + task: Task, + config: str, + *, + benchmark: str = "dry_run", + run_idx: int = 0, + model_name: str = "anthropic/claude-sonnet-4-5", + step_limit: int = 50, + cost_limit: float = 3.0, + wall_time_limit_seconds: int = 1200, + dry_run: bool = False, +) -> dict[str, Any]: + """Execute a single (task, config) pair. + + Returns a dict with keys: + metrics — a `bench.metrics.TaskMetrics` instance. + trajectory — the full mini-swe-agent `agent.serialize()` dict. + exit_status — "ok" | "error". + exit_reason — exception detail if exit_status == "error". + diff — `git diff` of the working tree after the run. + """ + if config not in VALID_CONFIGS: + raise ValueError(f"unknown config {config!r}; expected one of {VALID_CONFIGS}") + + # Late imports — they trigger litellm side effects. + from minisweagent.agents.default import DefaultAgent + from minisweagent.environments.local import LocalEnvironment + + env_vars = config_env(config, task.repo_path) + env = LocalEnvironment(cwd=str(task.repo_path), env=env_vars, timeout=120) + preamble = load_preamble(config) + + if dry_run: + model: Any = _DryRunModel() + else: + from minisweagent.models.litellm_model import LitellmModel + + model = LitellmModel(model_name=model_name) + + agent = DefaultAgent( + model, + env, + system_template=preamble, + instance_template=INSTANCE_TEMPLATE, + step_limit=step_limit, + cost_limit=cost_limit, + wall_time_limit_seconds=wall_time_limit_seconds, + ) + + started = time.time() + exit_status = "ok" + exit_reason = "" + try: + agent.run(task=task.problem_statement) + except Exception as exc: # noqa: BLE001 — runner classifies all failures + exit_status = "error" + exit_reason = f"{type(exc).__name__}: {exc}" + wall = time.time() - started + + diff = _capture_diff(task.repo_path) + trajectory = agent.serialize() + # Ensure the diff is part of the trajectory so the metrics module's + # patch extractor can find it even if the agent's final message + # didn't embed it. + trajectory.setdefault("info", {})["submission"] = diff + + from bench.metrics import TaskMetrics, task_metrics_from_trajectory + + metrics: TaskMetrics = task_metrics_from_trajectory( + trajectory, + benchmark=benchmark, + task_id=task.task_id, + config=config, + run_idx=run_idx, + wall_clock_sec=round(wall, 3), + ) + if exit_status == "error": + metrics.outcome = "error" + + return { + "metrics": metrics, + "trajectory": trajectory, + "exit_status": exit_status, + "exit_reason": exit_reason, + "diff": diff, + } + + +# --------------------------------------------------------------------------- +# Batch driver +# --------------------------------------------------------------------------- + + +def _write_trajectory(task_id: str, config: str, trajectory: dict[str, Any], + traj_dir: Path) -> Path: + traj_dir.mkdir(parents=True, exist_ok=True) + path = traj_dir / f"{task_id}__{config}.json" + path.write_text(json.dumps(trajectory, indent=2, sort_keys=True, default=str)) + return path + + +def run_batch( + tasks: list[Task], + configs: list[str], + *, + benchmark: str = "dry_run", + results_path: Path = DEFAULT_RESULTS, + trajectories_dir: Path = DEFAULT_CACHE_DIR / "trajectories", + **run_kwargs: Any, +) -> list[dict[str, Any]]: + """Run every (task, config) combination and append rows to a JSONL file.""" + from bench.metrics import append_jsonl + + results_path.parent.mkdir(parents=True, exist_ok=True) + rows: list[dict[str, Any]] = [] + for task in tasks: + for cfg in configs: + res = run_task(task, cfg, benchmark=benchmark, **run_kwargs) + _write_trajectory(task.task_id, cfg, res["trajectory"], trajectories_dir) + append_jsonl(results_path, res["metrics"]) + rows.append(res) + return rows + + +# --------------------------------------------------------------------------- +# Dry-run dataset +# --------------------------------------------------------------------------- + + +def _make_dry_run_task(tmp: Path) -> Task: + """A trivially tiny synthetic repo used by --dry-run.""" + tmp.mkdir(parents=True, exist_ok=True) + (tmp / "hello.py").write_text("print('hi')\n") + subprocess.run(["git", "init", "-q"], cwd=tmp, check=False) + subprocess.run(["git", "add", "."], cwd=tmp, check=False) + subprocess.run( + ["git", "-c", "user.email=b@b", "-c", "user.name=b", "commit", "-q", "-m", "init"], + cwd=tmp, check=False, + ) + return Task( + task_id="dry-run-1", + repo_name="dry-run", + repo_path=tmp, + problem_statement="No-op task: just confirm the harness works end to end.", + ) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="code-graph benchmark runner") + p.add_argument("--config", choices=VALID_CONFIGS, action="append", + help="one of baseline / lsp / code_graph; repeatable. " + "Default: all three.") + p.add_argument("--dry-run", action="store_true", + help="Use the stub LLM and a synthetic 1-task dataset; no API key needed.") + p.add_argument("--results", type=Path, default=DEFAULT_RESULTS) + p.add_argument("--trajectories", type=Path, default=DEFAULT_CACHE_DIR / "trajectories") + p.add_argument("--model", default="anthropic/claude-sonnet-4-5") + p.add_argument("--step-limit", type=int, default=50) + p.add_argument("--cost-limit", type=float, default=3.0) + p.add_argument("--wall-time", type=int, default=1200) + args = p.parse_args(argv) + + configs = args.config or list(VALID_CONFIGS) + + if not args.dry_run: + sys.stderr.write( + "Non-dry-run mode is not wired to SWE-bench yet — that lands when " + "the Anthropic key is available. Use --dry-run for now.\n" + ) + return 2 + + import tempfile + + with tempfile.TemporaryDirectory() as td: + task = _make_dry_run_task(Path(td) / "repo") + rows = run_batch( + [task], + configs, + results_path=args.results, + trajectories_dir=args.trajectories, + model_name=args.model, + step_limit=args.step_limit, + cost_limit=args.cost_limit, + wall_time_limit_seconds=args.wall_time, + dry_run=True, + ) + + for row in rows: + m = row["metrics"] + print( + f"[{m.config:>10}] {m.task_id} " + f"exit={row['exit_status']} " + f"in={m.input_tokens} out={m.output_tokens} " + f"tool_calls={m.tool_calls_total} wall={m.wall_clock_sec}s" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/tools/baseline/system_preamble.md b/bench/tools/baseline/system_preamble.md new file mode 100644 index 00000000..3ac81c69 --- /dev/null +++ b/bench/tools/baseline/system_preamble.md @@ -0,0 +1,20 @@ +# Baseline preamble + +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + +You have **no** code-navigation tools beyond what a stock Unix shell +gives you. Use `cat`, `grep`/`rg`, `find`, `sed`, and standard editing +to read and modify the codebase. + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. diff --git a/bench/tools/code_graph/system_preamble.md b/bench/tools/code_graph/system_preamble.md new file mode 100644 index 00000000..e02ce2d2 --- /dev/null +++ b/bench/tools/code_graph/system_preamble.md @@ -0,0 +1,39 @@ +# code-graph preamble + +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + +In addition to standard Unix tools (`cat`, `grep`/`rg`, `find`, `sed`), +you have a `cg` command on PATH that talks to a code-graph service +(`$CODEGRAPH_URL`) holding a knowledge graph of this repository. +Sub-commands: + +- `cg find-symbol --repo REPO --name NAME` — locate the node(s) + for a symbol by name. Returns `{id, label, file, line}` records. +- `cg get-neighbors --repo REPO --ids N [N ...]` — fetch direct + neighbors of the given node ids in the knowledge graph (callers, + callees, definitions, etc.). +- `cg find-paths --repo REPO --src N --dst N` — paths in the + graph between two nodes. +- `cg graph-entities --repo REPO` — paginated dump of the repo's + sub-graph (use sparingly — large output). +- `cg auto-complete --repo REPO --prefix STRING` — prefix-search + over symbol names. +- `cg note-edit --repo REPO --path PATH` — tell the service a + file changed; it will re-index that file. **Call this after every + edit** so subsequent graph queries reflect your changes. + +`REPO` is the repository name as registered in the service (set the +`REPO_NAME` environment variable if unsure: it is exported for you). + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. diff --git a/bench/tools/lsp/system_preamble.md b/bench/tools/lsp/system_preamble.md new file mode 100644 index 00000000..59e1ca3b --- /dev/null +++ b/bench/tools/lsp/system_preamble.md @@ -0,0 +1,33 @@ +# LSP preamble + +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + +In addition to standard Unix tools (`cat`, `grep`/`rg`, `find`, `sed`), +you have an `lsp` command on PATH that wraps a Python language server +(jedi via multilspy). Sub-commands: + +- `lsp goto-definition --file PATH --line N --col N` — locate the + definition of the symbol at the given position. +- `lsp find-references --file PATH --line N --col N` — find every + reference to the symbol at the given position (capped at 50). +- `lsp hover --file PATH --line N --col N` — return the + trimmed hover signature/doc for the symbol. +- `lsp document-symbols --file PATH` — outline of classes, functions, + and top-level symbols in the file. + +Lines and columns are **zero-indexed**. Paths are relative to the +repo root. Each `lsp` call starts its own language-server subprocess +(~1-3 seconds), so batch where you can. + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. diff --git a/pyproject.toml b/pyproject.toml index 3303a1a7..a671c10c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ test = [ "ruff>=0.11.0,<1.0.0", "httpx>=0.28.0,<1.0.0", ] +bench = [ + "mini-swe-agent>=1.0.0", +] [tool.pytest.ini_options] markers = [ diff --git a/tests/test_bench_runner.py b/tests/test_bench_runner.py new file mode 100644 index 00000000..bad65684 --- /dev/null +++ b/tests/test_bench_runner.py @@ -0,0 +1,150 @@ +"""Tests for `bench.runners.mini_runner` and the bash-tool CLI shims. + +These tests run **without** an LLM API key. They use the runner's +`--dry-run` mode (stub model) to validate: + +- the full mini-swe-agent loop executes cleanly, +- the trajectory is captured and persisted, +- the metrics module extracts tokens + tool calls from the + mini-swe-agent trajectory shape, +- the per-config env wiring (PATH for lsp/code_graph, baseline + PATH untouched) reaches the bash subprocess. + +We deliberately keep these tests off the network and off any LLM. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +from bench.runners import mini_runner + + +def _make_repo(tmp_path: Path) -> mini_runner.Task: + repo = tmp_path / "repo" + return mini_runner._make_dry_run_task(repo) + + +# --------------------------------------------------------------------------- +# run_task +# --------------------------------------------------------------------------- + + +def test_dry_run_task_baseline_completes(tmp_path: Path) -> None: + task = _make_repo(tmp_path) + res = mini_runner.run_task(task, "baseline", dry_run=True, step_limit=2) + assert res["exit_status"] == "ok" + m = res["metrics"] + assert m.benchmark == "dry_run" + assert m.config == "baseline" + assert m.input_tokens > 0 + assert m.output_tokens > 0 + assert m.tool_calls_total == 1 + # The stub submits via `printf ...COMPLETE_TASK...` so the bucket name + # is `submit`, not the raw printf. + assert "submit" in m.tool_calls_by_name + + +def test_dry_run_task_lsp_sets_repo_root(tmp_path: Path) -> None: + task = _make_repo(tmp_path) + res = mini_runner.run_task(task, "lsp", dry_run=True, step_limit=2) + assert res["exit_status"] == "ok" + # Trajectory must contain a bash output line with the cli dir on PATH. + text = json.dumps(res["trajectory"]) + assert "bench/cli" in text + + +def test_dry_run_unknown_config_raises(tmp_path: Path) -> None: + task = _make_repo(tmp_path) + with pytest.raises(ValueError, match="unknown config"): + mini_runner.run_task(task, "not_a_config", dry_run=True) + + +# --------------------------------------------------------------------------- +# config_env +# --------------------------------------------------------------------------- + + +def test_config_env_baseline_does_not_add_cli_dir(tmp_path: Path) -> None: + env = mini_runner.config_env("baseline", tmp_path) + assert str(mini_runner.CLI_DIR) not in env["PATH"] + + +def test_config_env_lsp_prepends_cli_dir_and_sets_repo_root(tmp_path: Path) -> None: + env = mini_runner.config_env("lsp", tmp_path) + assert env["PATH"].startswith(str(mini_runner.CLI_DIR)) + assert env["LSP_REPO_ROOT"] == str(tmp_path) + assert env["LSP_LANGUAGE"] == "python" + + +def test_config_env_code_graph_prepends_cli_dir_and_sets_url(tmp_path: Path) -> None: + env = mini_runner.config_env("code_graph", tmp_path) + assert env["PATH"].startswith(str(mini_runner.CLI_DIR)) + assert "CODEGRAPH_URL" in env + + +# --------------------------------------------------------------------------- +# run_batch persistence +# --------------------------------------------------------------------------- + + +def test_run_batch_writes_results_jsonl_and_trajectories(tmp_path: Path) -> None: + task = _make_repo(tmp_path) + results = tmp_path / "results.jsonl" + trajs = tmp_path / "trajs" + rows = mini_runner.run_batch( + [task], + ["baseline", "lsp"], + results_path=results, + trajectories_dir=trajs, + dry_run=True, + step_limit=2, + ) + assert len(rows) == 2 + assert results.exists() + lines = results.read_text().strip().splitlines() + assert len(lines) == 2 + for line in lines: + row = json.loads(line) + assert row["benchmark"] == "dry_run" + assert row["task_id"] == "dry-run-1" + # Trajectories on disk, one per (task, config). + assert (trajs / "dry-run-1__baseline.json").exists() + assert (trajs / "dry-run-1__lsp.json").exists() + + +# --------------------------------------------------------------------------- +# CLI smoke tests (argparse only — no service contact) +# --------------------------------------------------------------------------- + + +def test_cg_cli_help_exits_zero() -> None: + out = subprocess.run( + ["uv", "run", "python", "-m", "bench.cli.cg", "--help"], + capture_output=True, text=True, cwd=mini_runner.REPO_ROOT, check=False, + ) + assert out.returncode == 0 + assert "graph-entities" in out.stdout + assert "find-paths" in out.stdout + + +def test_lsp_cli_help_exits_zero() -> None: + out = subprocess.run( + ["uv", "run", "python", "-m", "bench.cli.lsp", "--help"], + capture_output=True, text=True, cwd=mini_runner.REPO_ROOT, check=False, + ) + assert out.returncode == 0 + assert "goto-definition" in out.stdout + assert "document-symbols" in out.stdout + + +def test_cg_cli_rejects_unknown_subcommand() -> None: + out = subprocess.run( + ["uv", "run", "python", "-m", "bench.cli.cg", "nope"], + capture_output=True, text=True, cwd=mini_runner.REPO_ROOT, check=False, + ) + assert out.returncode != 0 diff --git a/uv.lock b/uv.lock index 7e93ea89..29cb8415 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,84 @@ version = 1 revision = 3 requires-python = ">=3.12, <3.14" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] [[package]] name = "annotated-doc" @@ -174,6 +252,9 @@ dependencies = [ ] [package.optional-dependencies] +bench = [ + { name = "mini-swe-agent" }, +] test = [ { name = "httpx" }, { name = "pytest" }, @@ -187,6 +268,7 @@ requires-dist = [ { name = "graphrag-sdk", specifier = ">=1.1.1,<2.0.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, + { name = "mini-swe-agent", marker = "extra == 'bench'", specifier = ">=1.0.0" }, { name = "multilspy", git = "https://github.com/AviAvni/multilspy.git?rev=python-init-params" }, { name = "pygit2", specifier = ">=1.17.0,<2.0.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, @@ -200,7 +282,7 @@ requires-dist = [ { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, { name = "validators", specifier = ">=0.35.0,<0.36.0" }, ] -provides-extras = ["test"] +provides-extras = ["test", "bench"] [[package]] name = "colorama" @@ -289,6 +371,49 @@ nvtx = [ { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] +[[package]] +name = "datasets" +version = "4.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/34/14cd8e76f907f7d4dca2334cfeec9f81d30fd15c25a015f99aaea694eaed/datasets-4.8.5.tar.gz", hash = "sha256:0f0c1c3d56ffff2c93b2f4c63c95bac94f3d7e8621aea2a2a576275233bba772", size = 605649, upload-time = "2026-04-27T15:43:57.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/99/00f3196036501b53032c4b1ab8337a0b978dee832ed276dae3815df4e8b5/datasets-4.8.5-py3-none-any.whl", hash = "sha256:5079900781719c0e063a8efdd2cd95a31ad0c63209178669cd23cf1b926149ff", size = 528973, upload-time = "2026-04-27T15:43:53.702Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "docstring-to-markdown" version = "0.17" @@ -331,6 +456,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] +[[package]] +name = "fastuuid" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, +] + [[package]] name = "filelock" version = "3.25.0" @@ -348,6 +503,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "fsspec" version = "2026.2.0" @@ -357,6 +569,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + [[package]] name = "gliner" version = "0.2.26" @@ -587,6 +804,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "litellm" +version = "1.86.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastuuid" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/7c/a94c15e1146f76a6e0dfe0394f824cdfeb9dadb988c6f698a35f6c1b8d4f/litellm-1.86.1.tar.gz", hash = "sha256:616100384073f2ddec1a5391b34c806c7c99e6af4511741434809caa46075e13", size = 15379863, upload-time = "2026-05-26T03:51:58.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/47/e6e874857f45ff47762c6c29ec3dad9ea4757300e034400642cab90e4982/litellm-1.86.1-py3-none-any.whl", hash = "sha256:54e52372d326c642e9cc76d39afffb9b22387cbff3694c5e2758c9f860bca2e9", size = 17012257, upload-time = "2026-05-26T03:51:49.991Z" }, +] + [[package]] name = "lsprotocol" version = "2023.0.1" @@ -612,6 +936,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -653,6 +982,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -662,6 +1003,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mini-swe-agent" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets" }, + { name = "jinja2" }, + { name = "litellm" }, + { name = "openai" }, + { name = "platformdirs" }, + { name = "prompt-toolkit" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "textual" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/49/cfc66c0ec44d829e1d01b9e756f54294854a51362f4d54c68b81c7b49928/mini_swe_agent-2.3.0.tar.gz", hash = "sha256:8aae4f2a603a1f971dd183a5ca5157016e130d6f67f0ff35ad148d014673d41f", size = 63780, upload-time = "2026-05-21T14:37:59.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/f5/941c20e2152cc3bd0a85773ab62640c1d0a71972bff1964e3d059eceddf9/mini_swe_agent-2.3.0-py3-none-any.whl", hash = "sha256:27f68d784d3a22768fc389f31a451bb55e5d00fa46e1a27032d029da35878988", size = 110649, upload-time = "2026-05-21T14:37:58.548Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -671,6 +1037,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "multilspy" version = "0.0.11" @@ -680,6 +1109,22 @@ dependencies = [ { name = "requests" }, ] +[[package]] +name = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + [[package]] name = "networkx" version = "3.6.1" @@ -906,6 +1351,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, ] +[[package]] +name = "openai" +version = "2.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -915,6 +1379,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pandas" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, +] + [[package]] name = "parso" version = "0.8.6" @@ -924,6 +1424,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -933,6 +1442,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + [[package]] name = "protobuf" version = "7.35.0" @@ -948,6 +1529,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/ef/50433d346c56657a70d27f156c7b349ac59a068b01de4eb796e747eecc43/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0", size = 171659, upload-time = "2026-05-19T23:02:27.842Z" }, ] +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1142,6 +1752,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "regex" version = "2026.2.28" @@ -1215,15 +1839,67 @@ wheels = [ [[package]] name = "rich" -version = "13.9.4" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, ] [[package]] @@ -1373,6 +2049,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -1398,6 +2083,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "textual" +version = "8.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -1677,6 +2388,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -1786,6 +2515,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, ] +[[package]] +name = "wcwidth" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, +] + [[package]] name = "websockets" version = "16.0" @@ -1813,6 +2551,124 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + [[package]] name = "zipp" version = "3.23.0" From 57a406d511220f7c14273c341f47382af0e5dec4 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 22:46:01 +0300 Subject: [PATCH 18/63] Add --real-run mode with synthetic smoke task and outcome verification Adds --real-run as a mutually exclusive sibling of --dry-run. Real-run prepares a fresh repo per config (no cross-contamination), runs the agent against a synthetic buggy math_utils.py + pytest, then runs pytest to set metrics.outcome to resolved/failed. JSONL append in run_batch can now be deferred via defer_jsonl=True so the smoke loop can write the row once outcome is known. Validated end-to-end against GitHub Models (gpt-4o-mini) using GITHUB_API_KEY=$(gh auth token). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 150 +++++++++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 25 deletions(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 6eb74cf0..530c8520 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -339,13 +339,15 @@ def run_batch( """Run every (task, config) combination and append rows to a JSONL file.""" from bench.metrics import append_jsonl + defer_jsonl = run_kwargs.pop("defer_jsonl", False) results_path.parent.mkdir(parents=True, exist_ok=True) rows: list[dict[str, Any]] = [] for task in tasks: for cfg in configs: res = run_task(task, cfg, benchmark=benchmark, **run_kwargs) _write_trajectory(task.task_id, cfg, res["trajectory"], trajectories_dir) - append_jsonl(results_path, res["metrics"]) + if not defer_jsonl: + append_jsonl(results_path, res["metrics"]) rows.append(res) return rows @@ -356,7 +358,12 @@ def run_batch( def _make_dry_run_task(tmp: Path) -> Task: - """A trivially tiny synthetic repo used by --dry-run.""" + """A trivially tiny synthetic repo used by --dry-run. + + The dry-run stub model just submits immediately, so this repo + doesn't need to be solvable — it only needs to be a valid git + working tree so `git diff` and `LocalEnvironment` cwd work. + """ tmp.mkdir(parents=True, exist_ok=True) (tmp / "hello.py").write_text("print('hi')\n") subprocess.run(["git", "init", "-q"], cwd=tmp, check=False) @@ -373,6 +380,68 @@ def _make_dry_run_task(tmp: Path) -> Task: ) +def _make_synthetic_smoke_task(tmp: Path) -> Task: + """A tiny but **non-trivial** synthetic task used by --real-run. + + Unlike `_make_dry_run_task` (no-op for the stub model), this task + requires the agent to actually *do* something: read a file, find + a bug, and submit a patch. It's deliberately ~2-minute work for a + competent LLM — enough to exercise the trajectory, tool-call, + token-accounting, and diff-capture paths end-to-end, without the + cost or time of a SWE-bench task. + """ + tmp.mkdir(parents=True, exist_ok=True) + (tmp / "math_utils.py").write_text( + "def add(a, b):\n" + " # BUG: should return a + b\n" + " return a - b\n" + "\n" + "def multiply(a, b):\n" + " return a * b\n" + ) + (tmp / "test_math_utils.py").write_text( + "from math_utils import add, multiply\n" + "\n" + "def test_add():\n" + " assert add(2, 3) == 5\n" + " assert add(0, 0) == 0\n" + " assert add(-1, 1) == 0\n" + "\n" + "def test_multiply():\n" + " assert multiply(2, 3) == 6\n" + ) + subprocess.run(["git", "init", "-q"], cwd=tmp, check=False) + subprocess.run(["git", "add", "."], cwd=tmp, check=False) + subprocess.run( + ["git", "-c", "user.email=b@b", "-c", "user.name=b", "commit", "-q", "-m", "init"], + cwd=tmp, check=False, + ) + return Task( + task_id="smoke-add-bug", + repo_name="smoke-synthetic", + repo_path=tmp, + problem_statement=( + "The `add` function in math_utils.py is buggy: it subtracts " + "instead of adding. Fix it so that `pytest test_math_utils.py` " + "passes. Do not modify the tests." + ), + ) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def _verify_smoke_task(repo_path: Path) -> tuple[bool, str]: + """Run the smoke task's pytest. Returns (resolved, output).""" + res = subprocess.run( + ["python", "-m", "pytest", "test_math_utils.py", "-q"], + cwd=repo_path, capture_output=True, text=True, check=False, timeout=60, + ) + return res.returncode == 0, res.stdout + res.stderr + + # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -383,11 +452,23 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--config", choices=VALID_CONFIGS, action="append", help="one of baseline / lsp / code_graph; repeatable. " "Default: all three.") - p.add_argument("--dry-run", action="store_true", - help="Use the stub LLM and a synthetic 1-task dataset; no API key needed.") + mode = p.add_mutually_exclusive_group(required=True) + mode.add_argument("--dry-run", action="store_true", + help="Stub LLM + no-op synthetic task. No API key needed.") + mode.add_argument("--real-run", action="store_true", + help="Real LLM + a tiny synthetic 'add' bug task. " + "Validates real token accounting and the actual " + "model loop without paying for SWE-bench. " + "Requires an LLM API key for the chosen --model.") p.add_argument("--results", type=Path, default=DEFAULT_RESULTS) p.add_argument("--trajectories", type=Path, default=DEFAULT_CACHE_DIR / "trajectories") - p.add_argument("--model", default="anthropic/claude-sonnet-4-5") + p.add_argument("--model", default="anthropic/claude-sonnet-4-5", + help="litellm model name. Examples: " + "'anthropic/claude-sonnet-4-5' (needs ANTHROPIC_API_KEY); " + "'github/openai/gpt-4o-mini' (free GitHub Models, " + "needs GITHUB_TOKEN with models:read scope); " + "'github_copilot/gpt-4o' (uses your Copilot session, " + "device-code OAuth on first call).") p.add_argument("--step-limit", type=int, default=50) p.add_argument("--cost-limit", type=float, default=3.0) p.add_argument("--wall-time", type=int, default=1200) @@ -395,36 +476,55 @@ def main(argv: list[str] | None = None) -> int: configs = args.config or list(VALID_CONFIGS) - if not args.dry_run: - sys.stderr.write( - "Non-dry-run mode is not wired to SWE-bench yet — that lands when " - "the Anthropic key is available. Use --dry-run for now.\n" - ) - return 2 - import tempfile with tempfile.TemporaryDirectory() as td: - task = _make_dry_run_task(Path(td) / "repo") - rows = run_batch( - [task], - configs, - results_path=args.results, - trajectories_dir=args.trajectories, - model_name=args.model, - step_limit=args.step_limit, - cost_limit=args.cost_limit, - wall_time_limit_seconds=args.wall_time, - dry_run=True, - ) + if args.dry_run: + task_fn = _make_dry_run_task + benchmark = "dry_run" + dry_run = True + else: + task_fn = _make_synthetic_smoke_task + benchmark = "synthetic_smoke" + dry_run = False + + # Re-prep the repo per config so configs don't pollute each other's + # working trees. Each config sees the original buggy code. + rows: list[dict[str, Any]] = [] + verify_results: dict[str, bool] = {} + for cfg in configs: + repo_path = Path(td) / f"repo-{cfg}" + task = task_fn(repo_path) + cfg_rows = run_batch( + [task], + [cfg], + benchmark=benchmark, + results_path=args.results, + trajectories_dir=args.trajectories, + model_name=args.model, + step_limit=args.step_limit, + cost_limit=args.cost_limit, + wall_time_limit_seconds=args.wall_time, + dry_run=dry_run, + defer_jsonl=args.real_run, + ) + rows.extend(cfg_rows) + if args.real_run: + from bench.metrics import append_jsonl + + ok, _ = _verify_smoke_task(repo_path) + verify_results[cfg] = ok + cfg_rows[-1]["metrics"].outcome = "resolved" if ok else "failed" + append_jsonl(args.results, cfg_rows[-1]["metrics"]) for row in rows: m = row["metrics"] + verdict = f" outcome={m.outcome}" if m.outcome else "" print( f"[{m.config:>10}] {m.task_id} " f"exit={row['exit_status']} " f"in={m.input_tokens} out={m.output_tokens} " - f"tool_calls={m.tool_calls_total} wall={m.wall_clock_sec}s" + f"tool_calls={m.tool_calls_total} wall={m.wall_clock_sec}s{verdict}" ) return 0 From 020cf64cecd4c8b00c249129fad71c6839885dfb Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 22:50:53 +0300 Subject: [PATCH 19/63] Add SWE-bench Verified dataset loader and --swe-bench runner mode Loads princeton-nlp/SWE-bench_Verified via 'datasets', samples deterministically by seed (20260526) into smoke/calibration/headline stages (3/10/37), and prepares per-instance worktrees by cloning the upstream repo, checking out base_commit, and applying test_patch so FAIL_TO_PASS tests are present. Adds 'datasets' to the bench optional dep group. Adds 'swe_bench' mode to mini_runner alongside dry_run / real_run (mutually exclusive). Verification uses pytest with the FAIL_TO_PASS + PASS_TO_PASS test ids from the dataset row -- best effort because the official harness needs per-repo conda envs, which we don't build yet. 6 new unit tests cover the non-network parts of the loader (field parsing, sampling determinism, n override, pool clamping, path hygiene, task mapping). Worktree prep was validated end-to-end against pytest-dev/pytest-6202. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/datasets/__init__.py | 1 + bench/datasets/swe_bench.py | 244 +++++++++++++++++++++++++++++++++ bench/runners/mini_runner.py | 120 +++++++++++----- pyproject.toml | 1 + tests/test_swe_bench_loader.py | 63 +++++++++ uv.lock | 36 ++--- 6 files changed, 412 insertions(+), 53 deletions(-) create mode 100644 bench/datasets/__init__.py create mode 100644 bench/datasets/swe_bench.py create mode 100644 tests/test_swe_bench_loader.py diff --git a/bench/datasets/__init__.py b/bench/datasets/__init__.py new file mode 100644 index 00000000..6c64a1bb --- /dev/null +++ b/bench/datasets/__init__.py @@ -0,0 +1 @@ +"""Benchmark dataset loaders.""" diff --git a/bench/datasets/swe_bench.py b/bench/datasets/swe_bench.py new file mode 100644 index 00000000..6f89373a --- /dev/null +++ b/bench/datasets/swe_bench.py @@ -0,0 +1,244 @@ +"""SWE-bench Verified dataset loader. + +Loads `princeton-nlp/SWE-bench_Verified` via the `datasets` library, +samples instances deterministically by seed, prepares each repo as a +git working tree at the task's base commit, and exposes them as +`bench.runners.mini_runner.Task` objects. + +Repo clones are cached under `bench/cache/repos/{owner__name}/` +(bare-ish clone with all refs). For each task we materialize a +disposable worktree at `bench/cache/worktrees/{instance_id}/` and +hard-reset it to `base_commit`. The test_patch (which adds/modifies +the FAIL_TO_PASS tests but NOT the source patch) is applied so the +agent can actually run the tests; this matches the official +SWE-bench evaluation harness. + +Verification follows SWE-bench's protocol: after the agent submits a +patch, run the FAIL_TO_PASS + PASS_TO_PASS test selection and check +that all FAIL_TO_PASS go from fail→pass and all PASS_TO_PASS stay +passing. We approximate this with `pytest ` since the +official harness needs Docker per-instance environments. + +NOTE: This loader prepares repos for the agent but does NOT set up +per-repo Python dependencies. Real SWE-bench evaluation requires +constructing the exact conda environment specified in the dataset +row's `environment_setup_commit`. For an open evaluation we'll need +to either (a) use the official `swebench` harness for verification +or (b) build per-repo conda envs on the fly. This module flags +verification as "skipped" until that path is wired up. +""" + +from __future__ import annotations + +import json +import os +import random +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + +from bench.runners.mini_runner import Task + +DATASET_NAME = "princeton-nlp/SWE-bench_Verified" +DEFAULT_CACHE_ROOT = Path(__file__).resolve().parents[1] / "cache" +REPOS_DIR = DEFAULT_CACHE_ROOT / "repos" +WORKTREES_DIR = DEFAULT_CACHE_ROOT / "worktrees" + +# Locked-in seed from plan / configs/default.yaml. +DEFAULT_SEED = 20260526 + +# Per-stage sample sizes (locked-in from plan). +STAGE_SIZES = {"smoke": 3, "calibration": 10, "headline": 37} + + +@dataclass(slots=True) +class SweBenchInstance: + """Subset of SWE-bench Verified fields we use.""" + + instance_id: str + repo: str # "owner/name" + base_commit: str + problem_statement: str + test_patch: str + fail_to_pass: list[str] + pass_to_pass: list[str] + environment_setup_commit: str + version: str + + +def _git(args: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess: + return subprocess.run( + ["git", *args], + cwd=str(cwd) if cwd else None, + capture_output=True, + text=True, + check=check, + ) + + +def _parse_list_field(value: Any) -> list[str]: + """SWE-bench stores FAIL_TO_PASS / PASS_TO_PASS as JSON strings.""" + if isinstance(value, list): + return list(value) + if isinstance(value, str): + return list(json.loads(value)) + raise TypeError(f"unsupported list field: {type(value)!r}") + + +def load_instances( + *, + split: str = "test", + cache_dir: Path | None = None, +) -> list[SweBenchInstance]: + """Load all SWE-bench Verified instances from HuggingFace.""" + from datasets import load_dataset # local import — heavy + + kwargs: dict[str, Any] = {"split": split} + if cache_dir is not None: + kwargs["cache_dir"] = str(cache_dir) + ds = load_dataset(DATASET_NAME, **kwargs) + + out: list[SweBenchInstance] = [] + for row in ds: + out.append( + SweBenchInstance( + instance_id=row["instance_id"], + repo=row["repo"], + base_commit=row["base_commit"], + problem_statement=row["problem_statement"], + test_patch=row["test_patch"], + fail_to_pass=_parse_list_field(row["FAIL_TO_PASS"]), + pass_to_pass=_parse_list_field(row["PASS_TO_PASS"]), + environment_setup_commit=row.get("environment_setup_commit") or "", + version=row.get("version") or "", + ) + ) + return out + + +def sample_instances( + instances: Iterable[SweBenchInstance], + *, + stage: str = "smoke", + seed: int = DEFAULT_SEED, + n: int | None = None, +) -> list[SweBenchInstance]: + """Deterministic stratified sample. + + `stage` selects a size from `STAGE_SIZES` unless `n` is given. + Sampling is plain `random.sample` with the locked-in seed so runs + are reproducible across machines. + """ + size = n if n is not None else STAGE_SIZES[stage] + pool = list(instances) + rng = random.Random(seed) + return rng.sample(pool, k=min(size, len(pool))) + + +# --------------------------------------------------------------------------- +# Repo materialization +# --------------------------------------------------------------------------- + + +def _repo_cache_path(repo: str, repos_dir: Path) -> Path: + safe = repo.replace("/", "__") + return repos_dir / safe + + +def _ensure_repo_clone(repo: str, repos_dir: Path) -> Path: + """Ensure we have a local clone of `owner/name`. Returns its path.""" + dest = _repo_cache_path(repo, repos_dir) + if (dest / ".git").exists(): + return dest + dest.parent.mkdir(parents=True, exist_ok=True) + url = f"https://github.com/{repo}.git" + _git(["clone", url, str(dest)]) + return dest + + +def prepare_worktree( + inst: SweBenchInstance, + *, + repos_dir: Path = REPOS_DIR, + worktrees_dir: Path = WORKTREES_DIR, + apply_test_patch: bool = True, +) -> Path: + """Materialize a fresh working tree at `inst.base_commit`. + + Uses `git clone` of the cached repo (cheap because clones share + objects). The test_patch is applied unless `apply_test_patch` is + False so the agent can actually run the FAIL_TO_PASS tests. + """ + src = _ensure_repo_clone(inst.repo, repos_dir) + + dest = worktrees_dir / inst.instance_id + if dest.exists(): + shutil.rmtree(dest) + dest.parent.mkdir(parents=True, exist_ok=True) + + # Make a local clone (shares objects via --shared-style alternates). + _git(["clone", str(src), str(dest)]) + _git(["fetch", "origin", inst.base_commit], cwd=dest, check=False) + _git(["checkout", "--detach", inst.base_commit], cwd=dest) + + if apply_test_patch and inst.test_patch.strip(): + patch_file = dest / ".swebench-test.patch" + patch_file.write_text(inst.test_patch) + try: + _git(["apply", "--allow-empty", str(patch_file)], cwd=dest) + except subprocess.CalledProcessError as exc: + # Some test_patches need 3-way merge; retry with -3. + res = subprocess.run( + ["git", "apply", "--3way", str(patch_file)], + cwd=dest, capture_output=True, text=True, + ) + if res.returncode != 0: + raise RuntimeError( + f"failed to apply test_patch for {inst.instance_id}: {exc.stderr}" + ) + patch_file.unlink(missing_ok=True) + + return dest + + +def instance_to_task(inst: SweBenchInstance, repo_path: Path) -> Task: + """Wrap a SWE-bench instance as a bench.runners Task.""" + return Task( + task_id=inst.instance_id, + repo_name=inst.repo, + repo_path=repo_path, + problem_statement=inst.problem_statement, + verify_cmd=None, # verification done via swe_bench.verify_instance + ) + + +# --------------------------------------------------------------------------- +# Verification (approximate — official harness needs Docker) +# --------------------------------------------------------------------------- + + +def verify_instance( + inst: SweBenchInstance, + repo_path: Path, + *, + python: str | None = None, +) -> tuple[bool, str]: + """Run FAIL_TO_PASS + PASS_TO_PASS tests against the patched repo. + + Returns (passed, summary). Best-effort: many SWE-bench repos need + bespoke conda envs we don't build here. If pytest itself fails to + collect, returns (False, "") so the runner records `failed` + and we know to investigate. + """ + py = python or os.environ.get("BENCH_REPO_PYTHON") or "python" + test_ids = list(inst.fail_to_pass) + list(inst.pass_to_pass) + if not test_ids: + return False, "no test ids in dataset row" + + cmd = [py, "-m", "pytest", "-q", "--no-header", "-p", "no:cacheprovider", *test_ids] + res = subprocess.run(cmd, cwd=str(repo_path), capture_output=True, text=True) + ok = res.returncode == 0 + summary = res.stdout[-500:] + res.stderr[-500:] + return ok, summary diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 530c8520..9c51a450 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -460,6 +460,15 @@ def main(argv: list[str] | None = None) -> int: "Validates real token accounting and the actual " "model loop without paying for SWE-bench. " "Requires an LLM API key for the chosen --model.") + mode.add_argument("--swe-bench", action="store_true", + help="Real LLM + SWE-bench Verified instances. " + "Use --stage smoke|calibration|headline to pick the " + "sample size. Each instance: real clone + checkout " + "+ test_patch apply + agent run + FAIL_TO_PASS/" + "PASS_TO_PASS pytest verification.") + p.add_argument("--stage", choices=("smoke", "calibration", "headline"), + default="smoke", + help="SWE-bench stage (sample size). Only used with --swe-bench.") p.add_argument("--results", type=Path, default=DEFAULT_RESULTS) p.add_argument("--trajectories", type=Path, default=DEFAULT_CACHE_DIR / "trajectories") p.add_argument("--model", default="anthropic/claude-sonnet-4-5", @@ -478,44 +487,83 @@ def main(argv: list[str] | None = None) -> int: import tempfile - with tempfile.TemporaryDirectory() as td: - if args.dry_run: - task_fn = _make_dry_run_task - benchmark = "dry_run" - dry_run = True - else: - task_fn = _make_synthetic_smoke_task - benchmark = "synthetic_smoke" - dry_run = False - - # Re-prep the repo per config so configs don't pollute each other's - # working trees. Each config sees the original buggy code. - rows: list[dict[str, Any]] = [] - verify_results: dict[str, bool] = {} - for cfg in configs: - repo_path = Path(td) / f"repo-{cfg}" - task = task_fn(repo_path) - cfg_rows = run_batch( - [task], - [cfg], - benchmark=benchmark, - results_path=args.results, - trajectories_dir=args.trajectories, - model_name=args.model, - step_limit=args.step_limit, - cost_limit=args.cost_limit, - wall_time_limit_seconds=args.wall_time, - dry_run=dry_run, - defer_jsonl=args.real_run, - ) - rows.extend(cfg_rows) - if args.real_run: - from bench.metrics import append_jsonl - - ok, _ = _verify_smoke_task(repo_path) - verify_results[cfg] = ok + rows: list[dict[str, Any]] = [] + + if args.swe_bench: + from bench.datasets.swe_bench import ( + load_instances, sample_instances, prepare_worktree, + instance_to_task, verify_instance, + ) + from bench.metrics import append_jsonl + + insts = sample_instances(load_instances(), stage=args.stage) + print(f"[swe-bench] stage={args.stage} sampling {len(insts)} instances") + for inst in insts: + for cfg in configs: + # Fresh worktree per (instance, config) to avoid cross-talk. + wt = prepare_worktree(inst) + # Rename so each cfg gets a distinct path. + cfg_wt = wt.parent / f"{inst.instance_id}__{cfg}" + if cfg_wt.exists(): + import shutil + shutil.rmtree(cfg_wt) + wt.rename(cfg_wt) + task = instance_to_task(inst, cfg_wt) + cfg_rows = run_batch( + [task], + [cfg], + benchmark="swe_bench_verified", + results_path=args.results, + trajectories_dir=args.trajectories, + model_name=args.model, + step_limit=args.step_limit, + cost_limit=args.cost_limit, + wall_time_limit_seconds=args.wall_time, + dry_run=False, + defer_jsonl=True, + ) + rows.extend(cfg_rows) + ok, summary = verify_instance(inst, cfg_wt) cfg_rows[-1]["metrics"].outcome = "resolved" if ok else "failed" + if not ok: + cfg_rows[-1]["verify_summary"] = summary[-200:] append_jsonl(args.results, cfg_rows[-1]["metrics"]) + else: + with tempfile.TemporaryDirectory() as td: + if args.dry_run: + task_fn = _make_dry_run_task + benchmark = "dry_run" + dry_run = True + else: + task_fn = _make_synthetic_smoke_task + benchmark = "synthetic_smoke" + dry_run = False + + verify_results: dict[str, bool] = {} + for cfg in configs: + repo_path = Path(td) / f"repo-{cfg}" + task = task_fn(repo_path) + cfg_rows = run_batch( + [task], + [cfg], + benchmark=benchmark, + results_path=args.results, + trajectories_dir=args.trajectories, + model_name=args.model, + step_limit=args.step_limit, + cost_limit=args.cost_limit, + wall_time_limit_seconds=args.wall_time, + dry_run=dry_run, + defer_jsonl=args.real_run, + ) + rows.extend(cfg_rows) + if args.real_run: + from bench.metrics import append_jsonl + + ok, _ = _verify_smoke_task(repo_path) + verify_results[cfg] = ok + cfg_rows[-1]["metrics"].outcome = "resolved" if ok else "failed" + append_jsonl(args.results, cfg_rows[-1]["metrics"]) for row in rows: m = row["metrics"] diff --git a/pyproject.toml b/pyproject.toml index a671c10c..2e975e99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ test = [ "httpx>=0.28.0,<1.0.0", ] bench = [ + "datasets>=4.8.5", "mini-swe-agent>=1.0.0", ] diff --git a/tests/test_swe_bench_loader.py b/tests/test_swe_bench_loader.py new file mode 100644 index 00000000..2ec6986e --- /dev/null +++ b/tests/test_swe_bench_loader.py @@ -0,0 +1,63 @@ +"""Unit tests for bench.datasets.swe_bench (no network).""" + +from __future__ import annotations + +import pytest + +from bench.datasets import swe_bench as sb + + +def _fake_inst(instance_id: str, repo: str = "x/y") -> sb.SweBenchInstance: + return sb.SweBenchInstance( + instance_id=instance_id, + repo=repo, + base_commit="deadbeef" * 5, + problem_statement="fix the thing", + test_patch="", + fail_to_pass=["t::a"], + pass_to_pass=["t::b"], + environment_setup_commit="", + version="0", + ) + + +def test_parse_list_field_handles_json_string_and_list(): + assert sb._parse_list_field('["a", "b"]') == ["a", "b"] + assert sb._parse_list_field(["a", "b"]) == ["a", "b"] + with pytest.raises(TypeError): + sb._parse_list_field(42) + + +def test_sample_instances_is_deterministic(): + pool = [_fake_inst(f"id-{i}") for i in range(50)] + a = sb.sample_instances(pool, stage="smoke") + b = sb.sample_instances(pool, stage="smoke") + assert [i.instance_id for i in a] == [i.instance_id for i in b] + assert len(a) == sb.STAGE_SIZES["smoke"] + + +def test_sample_instances_respects_explicit_n(): + pool = [_fake_inst(f"id-{i}") for i in range(50)] + out = sb.sample_instances(pool, stage="smoke", n=7) + assert len(out) == 7 + + +def test_sample_instances_clamps_to_pool_size(): + pool = [_fake_inst(f"id-{i}") for i in range(2)] + out = sb.sample_instances(pool, stage="headline") # asks for 37 + assert len(out) == 2 + + +def test_repo_cache_path_uses_safe_delimiter(tmp_path): + p = sb._repo_cache_path("astropy/astropy", tmp_path) + assert p == tmp_path / "astropy__astropy" + + +def test_instance_to_task_maps_fields(tmp_path): + inst = _fake_inst("foo") + task = sb.instance_to_task(inst, tmp_path) + assert task.task_id == "foo" + assert task.repo_name == "x/y" + assert task.repo_path == tmp_path + assert task.problem_statement == "fix the thing" + assert task.verify_cmd is None diff --git a/uv.lock b/uv.lock index 29cb8415..0064c007 100644 --- a/uv.lock +++ b/uv.lock @@ -253,6 +253,7 @@ dependencies = [ [package.optional-dependencies] bench = [ + { name = "datasets" }, { name = "mini-swe-agent" }, ] test = [ @@ -263,6 +264,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "datasets", marker = "extra == 'bench'", specifier = ">=4.8.5" }, { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, { name = "graphrag-sdk", specifier = ">=1.1.1,<2.0.0" }, @@ -314,7 +316,7 @@ name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, @@ -341,34 +343,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1179,7 +1181,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -1218,7 +1220,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -1230,7 +1232,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1260,9 +1262,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1274,7 +1276,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, From 7088e23c7d0d762aa98b52b9200ecabdd748b098 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Tue, 26 May 2026 22:59:37 +0300 Subject: [PATCH 20/63] Add report CLI and SWE-bench Docker verify adapter bench/report/__main__.py: `uv run python -m bench.report` renders results.jsonl as a per-config summary table with token-delta vs baseline. Validated against the existing real-run smoke results. bench/runners/swebench_verify.py: exports per-config predictions JSONL files in the SWE-bench harness format, optionally invokes `python -m swebench.harness.run_evaluation` (Docker-based), then parses the resulting report.json and patches outcomes back into results.jsonl. 4 new unit tests cover the non-Docker parts. Adds `swebench>=4.0` to the bench optional dep group. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/report/__main__.py | 49 ++++ bench/runners/swebench_verify.py | 188 +++++++++++++++ pyproject.toml | 1 + tests/test_swebench_verify.py | 84 +++++++ uv.lock | 389 ++++++++++++++++++++++++++++++- 5 files changed, 702 insertions(+), 9 deletions(-) create mode 100644 bench/report/__main__.py create mode 100644 bench/runners/swebench_verify.py create mode 100644 tests/test_swebench_verify.py diff --git a/bench/report/__main__.py b/bench/report/__main__.py new file mode 100644 index 00000000..fea565e0 --- /dev/null +++ b/bench/report/__main__.py @@ -0,0 +1,49 @@ +"""CLI: render bench/cache/results.jsonl as a markdown summary. + +Usage: + uv run python -m bench.report \\ + --input bench/cache/results.jsonl \\ + --output bench/cache/report.md + +Defaults to bench/cache/results.jsonl → bench/cache/report.md. Also +prints the rendered markdown to stdout so it can be piped to a PR +body or pasted into an issue. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from bench.report import aggregate_to_markdown, load_jsonl, render_markdown, summarize + +DEFAULT_INPUT = Path(__file__).resolve().parents[1] / "cache" / "results.jsonl" +DEFAULT_OUTPUT = Path(__file__).resolve().parents[1] / "cache" / "report.md" + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="render benchmark results as markdown") + p.add_argument("--input", type=Path, default=DEFAULT_INPUT) + p.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + p.add_argument("--quiet", action="store_true", help="don't echo markdown to stdout") + args = p.parse_args(argv) + + if not args.input.exists(): + print(f"error: input not found: {args.input}") + return 1 + + rows = load_jsonl(args.input) + if not rows: + print("error: input has no rows") + return 1 + md = render_markdown(summarize(rows)) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(md) + if not args.quiet: + print(md) + print(f"\n[report] {len(rows)} row(s) -> {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bench/runners/swebench_verify.py b/bench/runners/swebench_verify.py new file mode 100644 index 00000000..c3191539 --- /dev/null +++ b/bench/runners/swebench_verify.py @@ -0,0 +1,188 @@ +"""Convert our results.jsonl into SWE-bench predictions and run the +official Docker-based evaluation harness. + +The SWE-bench harness expects predictions in the form: + + {"instance_id": "...", "model_name_or_path": "...", "model_patch": "..."} + +one per line. We have one row per (instance_id, config) in +results.jsonl with `metrics.patch` already populated as a `git diff`. +This module: + +1. Exports per-config predictions JSONL files from results.jsonl. +2. Optionally shells out to `python -m swebench.harness.run_evaluation` + (requires the `swebench` package and Docker) for each config. +3. Reads the harness report JSON back and patches outcomes in + results.jsonl so the report aggregator picks them up. + +Usage: + # Export only (no harness invocation): + uv run python -m bench.runners.swebench_verify --export-only + + # Full eval (needs swebench + Docker): + uv run python -m bench.runners.swebench_verify --run-id smoke1 +""" + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import sys +from collections import defaultdict +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_RESULTS = REPO_ROOT / "bench" / "cache" / "results.jsonl" +DEFAULT_PRED_DIR = REPO_ROOT / "bench" / "cache" / "predictions" +DEFAULT_REPORT_DIR = REPO_ROOT / "bench" / "cache" / "harness_reports" + + +def _load_jsonl(path: Path) -> list[dict[str, Any]]: + return [json.loads(l) for l in path.read_text().splitlines() if l.strip()] + + +def _dump_jsonl(rows: list[dict[str, Any]], path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + + +def export_predictions( + results_path: Path = DEFAULT_RESULTS, + out_dir: Path = DEFAULT_PRED_DIR, + *, + benchmark: str = "swe_bench_verified", +) -> dict[str, Path]: + """Write per-config predictions JSONL files. Returns {config: path}.""" + rows = _load_jsonl(results_path) + by_cfg: dict[str, list[dict[str, Any]]] = defaultdict(list) + for r in rows: + if r.get("benchmark") != benchmark: + continue + if not r.get("patch"): + continue + by_cfg[r["config"]].append({ + "instance_id": r["task_id"], + "model_name_or_path": f"code-graph-bench-{r['config']}", + "model_patch": r["patch"], + }) + + out_dir.mkdir(parents=True, exist_ok=True) + paths: dict[str, Path] = {} + for cfg, preds in by_cfg.items(): + p = out_dir / f"{cfg}.jsonl" + _dump_jsonl(preds, p) + paths[cfg] = p + return paths + + +def run_harness( + predictions: Path, + *, + run_id: str, + dataset_name: str = "princeton-nlp/SWE-bench_Verified", + report_dir: Path = DEFAULT_REPORT_DIR, + max_workers: int = 4, + timeout: int = 1800, +) -> Path: + """Invoke swebench.harness.run_evaluation. Returns path to report JSON.""" + if shutil.which("docker") is None: + raise RuntimeError("docker not found in PATH — SWE-bench harness needs Docker") + report_dir.mkdir(parents=True, exist_ok=True) + cmd = [ + sys.executable, "-m", "swebench.harness.run_evaluation", + "--dataset_name", dataset_name, + "--predictions_path", str(predictions), + "--max_workers", str(max_workers), + "--run_id", run_id, + "--timeout", str(timeout), + ] + print(f"[harness] $ {' '.join(cmd)}") + subprocess.run(cmd, check=True, cwd=str(report_dir)) + # Harness writes reports to ./logs/run_evaluation/{run_id}/... and a + # top-level ..json with the rolled-up verdict. + candidates = list(report_dir.glob(f"*.{run_id}.json")) + if not candidates: + raise RuntimeError(f"no report json found under {report_dir} for run_id={run_id}") + return candidates[0] + + +def patch_outcomes_from_report( + report_path: Path, + results_path: Path = DEFAULT_RESULTS, + *, + config: str, +) -> int: + """Read a harness report and rewrite outcomes for `config` rows in results.jsonl. + + Report shape (rolled-up): + {"resolved_ids": [...], "unresolved_ids": [...], ...} + Returns the number of rows patched. + """ + report = json.loads(report_path.read_text()) + resolved = set(report.get("resolved_ids", [])) + unresolved = set(report.get("unresolved_ids", [])) + + rows = _load_jsonl(results_path) + n = 0 + for r in rows: + if r.get("config") != config: + continue + if r["task_id"] in resolved: + r["outcome"] = "resolved"; n += 1 + elif r["task_id"] in unresolved: + r["outcome"] = "failed"; n += 1 + _dump_jsonl(rows, results_path) + return n + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="SWE-bench Docker-based verification") + p.add_argument("--results", type=Path, default=DEFAULT_RESULTS) + p.add_argument("--pred-dir", type=Path, default=DEFAULT_PRED_DIR) + p.add_argument("--report-dir", type=Path, default=DEFAULT_REPORT_DIR) + p.add_argument("--run-id", default="bench-smoke", + help="harness run id (used as a logs/ subdir)") + p.add_argument("--max-workers", type=int, default=4) + p.add_argument("--timeout", type=int, default=1800) + p.add_argument("--export-only", action="store_true", + help="just write per-config predictions JSONL; don't run harness") + args = p.parse_args(argv) + + if not args.results.exists(): + print(f"error: results not found: {args.results}") + return 1 + + paths = export_predictions(args.results, args.pred_dir) + if not paths: + print("[export] no swe_bench_verified rows with patches in results.jsonl") + return 1 + print(f"[export] wrote predictions for configs: {sorted(paths)}") + for cfg, path in paths.items(): + n = sum(1 for _ in path.open()) + print(f" - {cfg}: {n} predictions -> {path}") + + if args.export_only: + return 0 + + for cfg, pred_path in paths.items(): + run_id = f"{args.run_id}-{cfg}" + try: + report = run_harness( + pred_path, run_id=run_id, report_dir=args.report_dir, + max_workers=args.max_workers, timeout=args.timeout, + ) + except Exception as exc: + print(f"[harness] {cfg}: FAILED — {exc}") + continue + n = patch_outcomes_from_report(report, args.results, config=cfg) + print(f"[harness] {cfg}: patched {n} outcomes from {report}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index 2e975e99..cd840469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ test = [ bench = [ "datasets>=4.8.5", "mini-swe-agent>=1.0.0", + "swebench>=4.0", ] [tool.pytest.ini_options] diff --git a/tests/test_swebench_verify.py b/tests/test_swebench_verify.py new file mode 100644 index 00000000..355ff3b3 --- /dev/null +++ b/tests/test_swebench_verify.py @@ -0,0 +1,84 @@ +"""Unit tests for the SWE-bench Docker verification adapter (no Docker).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from bench.runners import swebench_verify as sv + + +def _write_results(path: Path, rows: list[dict]) -> None: + with path.open("w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + + +def test_export_predictions_filters_by_benchmark_and_patch(tmp_path: Path): + results = tmp_path / "results.jsonl" + _write_results(results, [ + # swe-bench rows with patches → exported + {"benchmark": "swe_bench_verified", "config": "baseline", + "task_id": "django__django-1", "patch": "diff a"}, + {"benchmark": "swe_bench_verified", "config": "lsp", + "task_id": "django__django-1", "patch": "diff b"}, + # missing patch → skipped + {"benchmark": "swe_bench_verified", "config": "baseline", + "task_id": "django__django-2", "patch": ""}, + # wrong benchmark → skipped + {"benchmark": "synthetic_smoke", "config": "baseline", + "task_id": "smoke-1", "patch": "diff c"}, + ]) + out_dir = tmp_path / "preds" + paths = sv.export_predictions(results, out_dir) + assert set(paths) == {"baseline", "lsp"} + + baseline = [json.loads(l) for l in paths["baseline"].read_text().splitlines()] + assert len(baseline) == 1 + row = baseline[0] + assert row["instance_id"] == "django__django-1" + assert row["model_name_or_path"] == "code-graph-bench-baseline" + assert row["model_patch"] == "diff a" + + +def test_export_predictions_returns_empty_when_no_matching_rows(tmp_path: Path): + results = tmp_path / "results.jsonl" + _write_results(results, [ + {"benchmark": "synthetic_smoke", "config": "baseline", + "task_id": "smoke-1", "patch": "diff"} + ]) + paths = sv.export_predictions(results, tmp_path / "preds") + assert paths == {} + + +def test_patch_outcomes_from_report(tmp_path: Path): + results = tmp_path / "results.jsonl" + _write_results(results, [ + {"benchmark": "swe_bench_verified", "config": "baseline", + "task_id": "x-1", "patch": "p", "outcome": None}, + {"benchmark": "swe_bench_verified", "config": "baseline", + "task_id": "x-2", "patch": "p", "outcome": None}, + {"benchmark": "swe_bench_verified", "config": "lsp", + "task_id": "x-1", "patch": "p", "outcome": None}, + ]) + report = tmp_path / "r.json" + report.write_text(json.dumps({ + "resolved_ids": ["x-1"], "unresolved_ids": ["x-2"], + })) + + n = sv.patch_outcomes_from_report(report, results, config="baseline") + assert n == 2 + + rows = [json.loads(l) for l in results.read_text().splitlines()] + assert rows[0]["outcome"] == "resolved" + assert rows[1]["outcome"] == "failed" + # lsp row untouched + assert rows[2]["outcome"] is None + + +def test_run_harness_raises_when_docker_missing(tmp_path: Path, monkeypatch): + monkeypatch.setattr(sv.shutil, "which", lambda _: None) + with pytest.raises(RuntimeError, match="docker not found"): + sv.run_harness(tmp_path / "preds.jsonl", run_id="x") diff --git a/uv.lock b/uv.lock index 0064c007..8864a789 100644 --- a/uv.lock +++ b/uv.lock @@ -120,6 +120,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "cattrs" version = "26.1.0" @@ -133,6 +146,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, ] +[[package]] +name = "cbor2" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/db/810437bcfe13cf5e09b68bad1ce57c8fa04ca9272c68946bbf2f4fa522c8/cbor2-6.1.1.tar.gz", hash = "sha256:6f0644869e0fdcd6f3874330b8f1cebd009f33191de43acf609dc2409cd362c4", size = 86297, upload-time = "2026-05-14T10:57:42.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/16/ac4710211e506a522bfe522dc02d676f308cff24c512b375b10e1cff62ed/cbor2-6.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f72e5f7e42a92f5ad2486dd14431bd09f966d167fc9e61cecef6740acf1b451", size = 410055, upload-time = "2026-05-14T10:56:54.133Z" }, + { url = "https://files.pythonhosted.org/packages/13/3a/ae0df2f8e4f8fac9212a3a9684a6213b6ba3190cd7762d78e5bd5043dddb/cbor2-6.1.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a409b0b6de923f68f5e35287f25ec654fc68135991e41ae9a1c500ddd982c1fb", size = 453919, upload-time = "2026-05-14T10:56:55.468Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/f5b3feb35e942998f60545199ff9c4c80d552a8b783d07f7ff70e78e8b1f/cbor2-6.1.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:911b34263f39300dd8ec6b78f247b257caba0bbcd278bd2421a54d45595ff602", size = 467302, upload-time = "2026-05-14T10:56:56.76Z" }, + { url = "https://files.pythonhosted.org/packages/17/6d/a0472d99d9a38728498c9bcb4c65687383a948b0152e0bd7a20c1a87c949/cbor2-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:596418d033cff6eb0de9cb4ae63dd91c80e68d4ed01e1d0c61ad51709acc8ed2", size = 521305, upload-time = "2026-05-14T10:56:58.484Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/1d8cdb754def050e0d0674a556540d4a26bab0d7cfc3e11df14f2e4a2830/cbor2-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce0e9a33d7ee2c8f47ae216be68a3a0a4d6d9832594a69e34be070cf6d13a9d8", size = 534365, upload-time = "2026-05-14T10:56:59.85Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fe/eaac5df152999aad4f3b4c4a25d0268b422dfebdbec28ebde8d3668604c5/cbor2-6.1.1-cp312-cp312-win32.whl", hash = "sha256:835f789f526ca7e729a8957da5ff6f33dfbda6c0b068695d01872fd6e35bbec2", size = 282520, upload-time = "2026-05-14T10:57:01.177Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e4/f1e480dbe8c11f5edf86c123adc25cfaf2eea1e80740da99e9cef735ae8f/cbor2-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63ab065ae26e48d39fc6f4d7f44dd0780afdb91a70ffb8f33e281f54ee35ad14", size = 300677, upload-time = "2026-05-14T10:57:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f8/2534292515d113b1fb319e0bdc2ee508be9d9d2ce2389dbee00a66dfb97e/cbor2-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:199cbe1fa0326ec06f1d986bdfe488b3cafd2b1b5367a81c8f53c8364cab4803", size = 288811, upload-time = "2026-05-14T10:57:04.519Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ec/30a52d7f6844cefd37601311a226d091268564a47b0dac56bc0469573681/cbor2-6.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f027e077345ba7d1a88cbed9168196e77f5ce8e8c816305bb1c7a2e4894bddf", size = 409070, upload-time = "2026-05-14T10:57:05.843Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a5/653193249a64ca46def52798e8f10ddbc918f11818a977b2aa7248062520/cbor2-6.1.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:559025ad8e1f9f5d019a40dc8f14f43c111c11207b4dde852e943a3002b43ec0", size = 453218, upload-time = "2026-05-14T10:57:07.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/79/bdcb9d43ed537abaa89e662d6340244207ec85b6e66e3bd7f40856c3a5d4/cbor2-6.1.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a6690f7df210386866e120475183132df98f77bf6df624097f66e3214e775084", size = 466244, upload-time = "2026-05-14T10:57:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/9c/44/fe0543996d53538c074f8ee18f7391b5458c528b1717740d750a9e472e1d/cbor2-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4898b5463a567775a05310407dbea5b4a8d7ae8e81337ae9084f5fe226938ff", size = 520804, upload-time = "2026-05-14T10:57:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/cd/83/577bbafef3bc887d654a73f3f4ab11e1bd5320abd9108bfc51fbea1498a8/cbor2-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf3ef1fae6f14081a15f178e933ab846d3181f059ee4090975518b71f58bb09f", size = 533598, upload-time = "2026-05-14T10:57:12.098Z" }, + { url = "https://files.pythonhosted.org/packages/57/32/c1c9f435b109ded86ef2e90ff73b95624c84c6edf01489941363a6069725/cbor2-6.1.1-cp313-cp313-win32.whl", hash = "sha256:4642780d27c0b411f4669fcb82e0d7a6b93a0c41c03a0c51296fd6f6858f63fa", size = 281738, upload-time = "2026-05-14T10:57:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/4d/39/9232731f161b2dfe2dc28b06bbacfc2b6a85f1255bf58ebc578ae760ef38/cbor2-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:616bc0538095860fe5607cc06d7b2de3e261a6caccd01ff3f1d4a4a9ad29adbf", size = 300018, upload-time = "2026-05-14T10:57:15.021Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c2/67f2e3a83acfcecad947784bb1590d1978662b5472fcbf7d73e219813456/cbor2-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7b193d2d024bb5d037e613272f5e436d53f02301101f0ce3916117688643181f", size = 287823, upload-time = "2026-05-14T10:57:16.525Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -177,6 +214,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.5" @@ -255,6 +322,7 @@ dependencies = [ bench = [ { name = "datasets" }, { name = "mini-swe-agent" }, + { name = "swebench" }, ] test = [ { name = "httpx" }, @@ -276,6 +344,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, { name = "ruff", marker = "extra == 'test'", specifier = ">=0.11.0,<1.0.0" }, + { name = "swebench", marker = "extra == 'bench'", specifier = ">=4.0" }, { name = "tree-sitter", specifier = ">=0.25.2,<0.26.0" }, { name = "tree-sitter-c", specifier = ">=0.24.1,<0.25.0" }, { name = "tree-sitter-c-sharp", specifier = ">=0.23.1,<0.24.0" }, @@ -407,6 +476,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -416,6 +494,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, +] + [[package]] name = "docstring-to-markdown" version = "0.17" @@ -458,6 +550,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] +[[package]] +name = "fastcore" +version = "1.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/65/99f599a285033febf95f9c608d91d629ac5d9995f57e5b3ac3397097f440/fastcore-1.13.2.tar.gz", hash = "sha256:f660b3448de48ba31973b2866c994ea3cd5e0a654847f57d6911a1a4bffda777", size = 100337, upload-time = "2026-05-17T06:02:24.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/41/2c368f804bb9bd918da3b61324207fc4b410d0f32352c372c0680fc1f670/fastcore-1.13.2-py3-none-any.whl", hash = "sha256:2103c9e9e613311c0b36eab17299a221e778fd214ec526e8df1d32908928277c", size = 105060, upload-time = "2026-05-17T06:02:22.28Z" }, +] + [[package]] name = "fastuuid" version = "0.14.0" @@ -576,6 +677,42 @@ http = [ { name = "aiohttp" }, ] +[[package]] +name = "ghapi" +version = "1.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastcore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/09/1b88f97e8599cda096d42dac830bb2e28ddf202d71843f61bda52bbe99ce/ghapi-1.0.13.tar.gz", hash = "sha256:fb46f5e101efa33bd12a0ae7694de761eec5be1de90f48847699f1e00128f928", size = 72914, upload-time = "2026-02-28T02:21:01.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ac/e1960ec21cfd5a0fd9b329822c04d0b5f91abb688c3b1acd7e8ff3390432/ghapi-1.0.13-py3-none-any.whl", hash = "sha256:49d7e336e5664e4d4f92b1d442dfe80f31ecccbee4370bd1d271bd63a1ccf18e", size = 71409, upload-time = "2026-02-28T02:21:00.457Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + [[package]] name = "gliner" version = "0.2.26" @@ -612,6 +749,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/e9/29b5d7d94c077b0ec2254366b4bdf8a8a032240098c82909f4b1a4f99225/graphrag_sdk-1.1.1-py3-none-any.whl", hash = "sha256:4f18a833f39854883875e11c981bad5649a0f467280a19702e82b1c9938a7abe", size = 159565, upload-time = "2026-05-13T13:17:40.963Z" }, ] +[[package]] +name = "grpclib" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h2" }, + { name = "multidict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/28/5a2c299ec82a876a252c5919aa895a6f1d1d35c96417c5ce4a4660dc3a80/grpclib-0.4.9.tar.gz", hash = "sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46", size = 84798, upload-time = "2025-12-14T22:23:14.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063, upload-time = "2025-12-14T22:23:13.224Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -621,6 +771,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + [[package]] name = "hf-xet" version = "1.3.2" @@ -654,6 +817,15 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cf/7a/1a9b1405f2eb59515f06c3074750b03e0e96edf7fee0f6dd6df81d9c21d7/hnswlib-0.8.0.tar.gz", hash = "sha256:cb6d037eedebb34a7134e7dc78966441dfd04c9cf5ee93911be911ced951c44c", size = 36206, upload-time = "2023-12-03T04:16:17.55Z" } +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -724,6 +896,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1030,6 +1220,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/f5/941c20e2152cc3bd0a85773ab62640c1d0a71972bff1964e3d059eceddf9/mini_swe_agent-2.3.0-py3-none-any.whl", hash = "sha256:27f68d784d3a22768fc389f31a451bb55e5d00fa46e1a27032d029da35878988", size = 110649, upload-time = "2026-05-21T14:37:58.548Z" }, ] +[[package]] +name = "modal" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "cbor2" }, + { name = "certifi" }, + { name = "click" }, + { name = "grpclib" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "synchronicity" }, + { name = "toml" }, + { name = "types-certifi" }, + { name = "types-toml" }, + { name = "typing-extensions" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/7d/4126d0fe879ef3e86002ca821a34cb68a2588ea2e8ccb2bfe421d0f42ffe/modal-1.4.3.tar.gz", hash = "sha256:35b2fc840f759b512e12527afb538e1ea4cc232b84cfbfcef3f5d96d5a66abaa", size = 720488, upload-time = "2026-05-18T22:34:45.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/54/400262056c144ceee5edab40efa2541ae8928ae5f244fd9025f3ad26c909/modal-1.4.3-py3-none-any.whl", hash = "sha256:802917181f576458a0cb833322157dab09c4f367326426c5a732661a0c519577", size = 826232, upload-time = "2026-05-18T22:34:43.335Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -1136,6 +1350,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.4.6" @@ -1444,6 +1667,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1518,17 +1757,17 @@ wheels = [ [[package]] name = "protobuf" -version = "7.35.0" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/fd/5b1491d9e4b586d621c54f4c36b888714164b6875f8d6afa3f9072906a51/protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6", size = 458677, upload-time = "2026-05-19T23:02:29.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/ee/93d06e358a4aa32280b00e722d3ea0a1f25fc3cc5778d80581c9cca2c10e/protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda", size = 433225, upload-time = "2026-05-19T23:02:19.884Z" }, - { url = "https://files.pythonhosted.org/packages/8b/39/1c76c2da93f3c507e958e0aecee2391cc44d4625de6c728bbc555195b5a8/protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5", size = 328847, upload-time = "2026-05-19T23:02:22.3Z" }, - { url = "https://files.pythonhosted.org/packages/91/1a/39f7ce90a238c1a987a4d81ec26379e02ca0aff367de68e4a1fa474215b9/protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee", size = 344030, upload-time = "2026-05-19T23:02:23.591Z" }, - { url = "https://files.pythonhosted.org/packages/70/5b/6baf9008817964454055ff3fe65f1de0b5f1e26c80c82f7fb108b7cd4ea3/protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011", size = 327130, upload-time = "2026-05-19T23:02:24.637Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e5/e46adb0badc388bfb84877a5f9f026aff63f60e611016cf64dbe77e05446/protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6", size = 428946, upload-time = "2026-05-19T23:02:25.741Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ab/547fbd9e16d879dd13c167478f8ae0a83a428008ca07a5e06acdc23ad473/protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201", size = 439996, upload-time = "2026-05-19T23:02:26.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ef/50433d346c56657a70d27f156c7b349ac59a068b01de4eb796e747eecc43/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0", size = 171659, upload-time = "2026-05-19T23:02:27.842Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] @@ -1708,6 +1947,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1717,6 +1969,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2051,6 +2316,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2060,6 +2334,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -2073,6 +2356,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "swebench" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "chardet" }, + { name = "datasets" }, + { name = "docker" }, + { name = "ghapi" }, + { name = "gitpython" }, + { name = "modal" }, + { name = "pre-commit" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tqdm" }, + { name = "unidiff" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/e1/c997299ad7bf088876d30398203aa1eed7dec897670dc1aa35b1d748ffcc/swebench-4.1.0.tar.gz", hash = "sha256:5aaa6a92c2db1aa64892d28a47483ca46a45a15cf1d2df673d7744f71811dc9a", size = 134341, upload-time = "2025-09-11T02:58:00.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/67/981d8b642ac3eac7c8a7b7832ff8b2fb74f96b28b5fcd9a8979879e5c46d/swebench-4.1.0-py3-none-any.whl", hash = "sha256:1243776f720047cc9e20a427f7a52b75c13a07abda6154fb60fe77f82ec8af57", size = 157231, upload-time = "2025-09-11T02:57:58.953Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -2085,6 +2393,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "synchronicity" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/5e/50ea27817003665c7cc4f5bdad309f13d6329037f657848ee87fe06c3740/synchronicity-0.12.2.tar.gz", hash = "sha256:6fd605a5035d1ec74ce48fffaca80ea00345c84ca34223914e2436fb4f162ff9", size = 60018, upload-time = "2026-04-06T15:06:15.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/44/4f6ba4e2c171847e6f9a460213b196bbf26edea43d0e66889c7ccc55d368/synchronicity-0.12.2-py3-none-any.whl", hash = "sha256:9dbaca81fb7f2b57c6dea326e514e1c80e9ccfd9c9618515e84fa6091026273b", size = 41312, upload-time = "2026-04-06T15:06:14.459Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -2170,6 +2490,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "torch" version = "2.12.0" @@ -2369,6 +2698,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, ] +[[package]] +name = "types-certifi" +version = "2021.10.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, +] + +[[package]] +name = "types-toml" +version = "0.10.8.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/11/6ece999e91f2ccb848ab4420f3f4816e78ac0541f739e6864affdaaa5737/types_toml-0.10.8.20260518.tar.gz", hash = "sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe", size = 9419, upload-time = "2026-05-18T06:02:16.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/25/489751806bf5c95e4007f8e17409199c54d31e49ffbea07c5729b1286c8e/types_toml-0.10.8.20260518-py3-none-any.whl", hash = "sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303", size = 9669, upload-time = "2026-05-18T06:02:15.86Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2408,6 +2755,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] +[[package]] +name = "unidiff" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/48/81be0ac96e423a877754153699731ef439fd7b80b4c8b5425c94ed079ebd/unidiff-0.7.5.tar.gz", hash = "sha256:2e5f0162052248946b9f0970a40e9e124236bf86c82b70821143a6fc1dea2574", size = 20931, upload-time = "2023-03-10T01:05:39.185Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/54/57c411a6e8f7bd7848c8b66e4dcaffa586bf4c02e63f2280db0327a4e6eb/unidiff-0.7.5-py2.py3-none-any.whl", hash = "sha256:c93bf2265cc1ba2a520e415ab05da587370bc2a3ae9e0414329f54f0c2fc09e8", size = 14386, upload-time = "2023-03-10T01:05:36.594Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -2470,6 +2826,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, ] +[[package]] +name = "virtualenv" +version = "21.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, +] + [[package]] name = "watchfiles" version = "1.1.1" From dcf4ac022a89bb815a8912ac613d51ba2aba400a Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 10:31:16 +0300 Subject: [PATCH 21/63] Load .env in mini_runner; document Anthropic / Azure providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mini_runner.main() now calls dotenv.load_dotenv(.env) at the repo root if present, so users don't have to export ANTHROPIC_API_KEY / ANTHROPIC_API_BASE / GITHUB_API_KEY by hand each shell session. .env.template gains a documented block for the four supported provider configs we've actually tested or have credentials for: direct Anthropic, Azure AI Foundry's Anthropic-passthrough endpoint (/anthropic/v1/messages, x-api-key), GitHub Models, and Azure OpenAI. Most relevant for our setup: Azure AI Foundry → litellm's anthropic/ provider with a custom ANTHROPIC_API_BASE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .env.template | 30 ++++++++++++++++++++++++++++++ bench/runners/mini_runner.py | 12 ++++++++++++ 2 files changed, 42 insertions(+) diff --git a/.env.template b/.env.template index b5047c95..2966af0d 100644 --- a/.env.template +++ b/.env.template @@ -27,3 +27,33 @@ GEMINI_API_KEY= # Optional Uvicorn bind settings used by start.sh / make run-* HOST=0.0.0.0 PORT=5000 + +# --------------------------------------------------------------------------- +# Benchmark runner (bench/runners/mini_runner.py) credentials. +# Picked up automatically from .env at repo root. +# +# Pick ONE of these provider configs based on your model choice: +# +# 1) Anthropic API (direct): +# ANTHROPIC_API_KEY=sk-ant-... +# # ANTHROPIC_API_BASE is unset → uses api.anthropic.com +# +# 2) Azure AI Foundry's Anthropic-compatible passthrough +# (path /anthropic/v1/messages, x-api-key auth): +# ANTHROPIC_API_KEY= +# ANTHROPIC_API_BASE=https://.services.ai.azure.com/anthropic +# Then: --model anthropic/claude-sonnet-4-5 +# +# 3) GitHub Models (free tier, 8K-16K context cap on personal plans): +# GITHUB_API_KEY=$(gh auth token) +# GITHUB_API_BASE=https://models.github.ai/inference +# Then: --model github/openai/gpt-4o-mini +# +# 4) Azure OpenAI (chat completions API, not Anthropic): +# AZURE_API_KEY= +# AZURE_API_BASE=https://.openai.azure.com +# AZURE_API_VERSION=2024-10-21 +# Then: --model azure/ +# --------------------------------------------------------------------------- +# ANTHROPIC_API_KEY= +# ANTHROPIC_API_BASE= diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 9c51a450..4e2b6e05 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -448,6 +448,18 @@ def _verify_smoke_task(repo_path: Path) -> tuple[bool, str]: def main(argv: list[str] | None = None) -> int: + # Load .env from repo root if present, so users don't have to export + # provider creds manually. litellm picks up ANTHROPIC_API_KEY / + # ANTHROPIC_API_BASE / AZURE_API_KEY / GITHUB_API_KEY from process env. + try: + from dotenv import load_dotenv + + env_path = REPO_ROOT / ".env" + if env_path.exists(): + load_dotenv(env_path) + except ImportError: + pass + p = argparse.ArgumentParser(description="code-graph benchmark runner") p.add_argument("--config", choices=VALID_CONFIGS, action="append", help="one of baseline / lsp / code_graph; repeatable. " From a6b1b4837b0a99786632cc504b6d5f6f57ad250f Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 11:46:27 +0300 Subject: [PATCH 22/63] bench: wire cg/lsp shims, pre-index code-graph track, sharpen preambles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke run showed the agent invoked cg exactly once and lsp zero times across all three SWE-bench instances — because the bash shims didn't exist (the agent's `which cg` returned 'cg not found'). The differential between configs was therefore noise. Fixes: - Add executable bash shims bench/cli/{cg,lsp} that exec "$BENCH_PYTHON" -m bench.cli.{cg,lsp}. Runner exports BENCH_PYTHON = sys.executable so the venv (with httpx/multilspy) is used. - Export REPO_NAME for the code_graph config (worktree dirname). The preamble references it; nothing was setting it. - _ensure_indexed(): POST /api/analyze_folder for each code_graph worktree before running the task, so cg find-symbol returns real results. Skips re-indexing via /api/list_repos precheck. - Rewrite system preambles to instruct "use cg/lsp BEFORE grep" with an explicit typical-loop, not just a list of subcommands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/cli/cg | 6 +++ bench/cli/lsp | 6 +++ bench/runners/mini_runner.py | 47 +++++++++++++++++ bench/tools/code_graph/system_preamble.md | 62 +++++++++++++++-------- bench/tools/lsp/system_preamble.md | 34 ++++++++++--- 5 files changed, 125 insertions(+), 30 deletions(-) create mode 100755 bench/cli/cg create mode 100755 bench/cli/lsp diff --git a/bench/cli/cg b/bench/cli/cg new file mode 100755 index 00000000..b9fd108c --- /dev/null +++ b/bench/cli/cg @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Bash-callable entry point for the code-graph CLI. The runner puts +# bench/cli on PATH so the agent can invoke `cg ...` directly. +# BENCH_PYTHON is set by the runner to the venv's python; falls back +# to system python3 if missing (won't have deps; will fail loudly). +exec "${BENCH_PYTHON:-python3}" -m bench.cli.cg "$@" diff --git a/bench/cli/lsp b/bench/cli/lsp new file mode 100755 index 00000000..bdc31834 --- /dev/null +++ b/bench/cli/lsp @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Bash-callable entry point for the LSP CLI. The runner puts +# bench/cli on PATH so the agent can invoke `lsp ...` directly. +# BENCH_PYTHON is set by the runner to the venv's python; falls back +# to system python3 if missing (won't have deps; will fail loudly). +exec "${BENCH_PYTHON:-python3}" -m bench.cli.lsp "$@" diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 4e2b6e05..af1cbfe3 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -132,15 +132,56 @@ def config_env(config: str, repo_path: Path) -> dict[str, str]: ) # Make `python -m bench.cli.cg ...` work too, regardless of cwd. env["PYTHONPATH"] = str(REPO_ROOT) + os.pathsep + env.get("PYTHONPATH", "") + # Pin the python the bash shims invoke to this process's interpreter so + # the bench deps (requests, multilspy, ...) are available. + env["BENCH_PYTHON"] = sys.executable if config == "lsp": env["LSP_REPO_ROOT"] = str(repo_path) env.setdefault("LSP_LANGUAGE", "python") elif config == "code_graph": # The runner is responsible for ensuring the service is up. env.setdefault("CODEGRAPH_URL", "http://127.0.0.1:5000") + # The agent's preamble references $REPO_NAME — set it to the + # worktree dirname, which is what analyze_folder used as the id. + env["REPO_NAME"] = repo_path.name return env +def _ensure_indexed(repo_path: Path) -> None: + """Trigger /api/analyze_folder so `cg --repo ` returns data. + + The code-graph backend uses `Path(folder).name` as the repo identifier; + each (instance, config) worktree has a unique directory name like + `pytest-dev__pytest-6202__code_graph`, which becomes the `--repo` value + the agent passes to `cg`. We skip indexing if the repo already exists + in FalkorDB (cheap precheck against /api/list_repos). + """ + import httpx + + base = os.environ.get("CODEGRAPH_URL", "http://127.0.0.1:5000").rstrip("/") + repo_name = repo_path.name + token = os.environ.get("SECRET_TOKEN") or os.environ.get("CODEGRAPH_TOKEN") + headers = {"Authorization": f"Bearer {token}"} if token else {} + try: + with httpx.Client(timeout=10.0, headers=headers) as c: + r = c.get(f"{base}/api/list_repos") + if r.status_code == 200 and repo_name in (r.json() or {}).get("repositories", []): + print(f"[index] {repo_name} already indexed; skip") + return + print(f"[index] analyzing {repo_path} ...") + with httpx.Client(timeout=600.0, headers=headers) as c: + r = c.post( + f"{base}/api/analyze_folder", + json={"path": str(repo_path), "ignore": []}, + ) + if r.status_code != 200: + print(f"[index] WARN analyze_folder returned {r.status_code}: {r.text[:200]}") + else: + print(f"[index] indexed {repo_name}") + except Exception as exc: # noqa: BLE001 + print(f"[index] WARN failed to index {repo_name}: {exc!r}") + + # --------------------------------------------------------------------------- # Dry-run stub model # --------------------------------------------------------------------------- @@ -521,6 +562,12 @@ def main(argv: list[str] | None = None) -> int: shutil.rmtree(cfg_wt) wt.rename(cfg_wt) task = instance_to_task(inst, cfg_wt) + # For the code-graph track, the agent's `cg` commands query + # FalkorDB by repo name (= worktree dir name). The graph must + # exist before the task runs, otherwise every `cg find-symbol` + # call returns nothing and the agent abandons the tool. + if cfg == "code_graph": + _ensure_indexed(cfg_wt) cfg_rows = run_batch( [task], [cfg], diff --git a/bench/tools/code_graph/system_preamble.md b/bench/tools/code_graph/system_preamble.md index e02ce2d2..5fb892d5 100644 --- a/bench/tools/code_graph/system_preamble.md +++ b/bench/tools/code_graph/system_preamble.md @@ -4,28 +4,45 @@ You are an autonomous coding agent solving a software-engineering task. Your sole tool is bash: every action you take is a shell command that is executed in the repository's working directory. -In addition to standard Unix tools (`cat`, `grep`/`rg`, `find`, `sed`), -you have a `cg` command on PATH that talks to a code-graph service -(`$CODEGRAPH_URL`) holding a knowledge graph of this repository. -Sub-commands: - -- `cg find-symbol --repo REPO --name NAME` — locate the node(s) - for a symbol by name. Returns `{id, label, file, line}` records. -- `cg get-neighbors --repo REPO --ids N [N ...]` — fetch direct - neighbors of the given node ids in the knowledge graph (callers, - callees, definitions, etc.). -- `cg find-paths --repo REPO --src N --dst N` — paths in the - graph between two nodes. -- `cg graph-entities --repo REPO` — paginated dump of the repo's - sub-graph (use sparingly — large output). -- `cg auto-complete --repo REPO --prefix STRING` — prefix-search - over symbol names. -- `cg note-edit --repo REPO --path PATH` — tell the service a - file changed; it will re-index that file. **Call this after every - edit** so subsequent graph queries reflect your changes. - -`REPO` is the repository name as registered in the service (set the -`REPO_NAME` environment variable if unsure: it is exported for you). +## Code-navigation workflow — use this BEFORE grep/find + +A code-graph service is indexed for this repository. **Before reading or +editing code, locate the relevant symbols through `cg` rather than +grepping the file tree** — it's faster, returns precise file:line +records, and reveals call/definition relationships you would otherwise +have to reconstruct by hand. Fall back to bash only when `cg` cannot +answer the question. + +Typical loop: + +1. `cg find-symbol --repo "$REPO_NAME" --name ` to locate a + function/class by name. +2. `cg get-neighbors --repo "$REPO_NAME" --ids ` to see callers, + callees, and definitions. +3. Read the implicated file(s) with `sed -n` / `cat`, then edit. +4. After every file edit, run + `cg note-edit --repo "$REPO_NAME" --path ` so subsequent + graph queries reflect your change. + +`$REPO_NAME` is exported for you (do not guess). + +## Available `cg` sub-commands + +- `cg find-symbol --repo REPO --name NAME` — locate node(s) for a + symbol by name. Returns `{id, label, file, line}` records. +- `cg get-neighbors --repo REPO --ids N [N ...]` — direct neighbors + in the knowledge graph (callers, callees, definitions). +- `cg find-paths --repo REPO --src N --dst N` — paths between two + nodes. +- `cg graph-entities --repo REPO` — paginated sub-graph dump (large). +- `cg auto-complete --repo REPO --prefix STRING` — prefix search. +- `cg note-edit --repo REPO --path PATH` — re-index a file after + you edit it. **Call this after every edit.** + +You also have the usual Unix tools (`cat`, `grep`/`rg`, `find`, `sed`) +for cases the graph can't answer. + +## Submission When you believe the task is complete, run a bash command whose first line of stdout is exactly: @@ -37,3 +54,4 @@ COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT followed by your final answer or summary on subsequent lines. The runner reads the working-tree `git diff` automatically; you do not need to commit. + diff --git a/bench/tools/lsp/system_preamble.md b/bench/tools/lsp/system_preamble.md index 59e1ca3b..b1cfa61c 100644 --- a/bench/tools/lsp/system_preamble.md +++ b/bench/tools/lsp/system_preamble.md @@ -4,23 +4,40 @@ You are an autonomous coding agent solving a software-engineering task. Your sole tool is bash: every action you take is a shell command that is executed in the repository's working directory. -In addition to standard Unix tools (`cat`, `grep`/`rg`, `find`, `sed`), -you have an `lsp` command on PATH that wraps a Python language server -(jedi via multilspy). Sub-commands: +## Code-navigation workflow — use the LSP BEFORE grep + +An `lsp` command on PATH wraps a Python language server (jedi via +multilspy). **Prefer `lsp` over grep/find for "where is this defined?" +and "what calls this?"** — it follows imports and respects scope, +unlike textual search. + +Typical loop: + +1. Use `grep -rn` once to find a candidate file:line for a symbol you + want to investigate (jedi needs a concrete position to query). +2. `lsp goto-definition --file PATH --line N --col N` to jump to its + real definition. +3. `lsp find-references --file PATH --line N --col N` to enumerate + callers/usages before editing. +4. Read the implicated file(s) with `sed -n` / `cat`, then edit. + +## Available `lsp` sub-commands - `lsp goto-definition --file PATH --line N --col N` — locate the definition of the symbol at the given position. - `lsp find-references --file PATH --line N --col N` — find every - reference to the symbol at the given position (capped at 50). -- `lsp hover --file PATH --line N --col N` — return the - trimmed hover signature/doc for the symbol. + reference (capped at 50). +- `lsp hover --file PATH --line N --col N` — trimmed hover + signature/doc for the symbol. - `lsp document-symbols --file PATH` — outline of classes, functions, and top-level symbols in the file. -Lines and columns are **zero-indexed**. Paths are relative to the -repo root. Each `lsp` call starts its own language-server subprocess +Lines and columns are **zero-indexed**. Paths are relative to the repo +root. Each `lsp` call starts its own language-server subprocess (~1-3 seconds), so batch where you can. +## Submission + When you believe the task is complete, run a bash command whose first line of stdout is exactly: @@ -31,3 +48,4 @@ COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT followed by your final answer or summary on subsequent lines. The runner reads the working-tree `git diff` automatically; you do not need to commit. + From 03c7a7371fcef730f9110d1f8314d8d7e61d0771 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 12:00:58 +0300 Subject: [PATCH 23/63] bench: add --limit flag for quick single-instance runs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index af1cbfe3..86b8cbfe 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -522,6 +522,9 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--stage", choices=("smoke", "calibration", "headline"), default="smoke", help="SWE-bench stage (sample size). Only used with --swe-bench.") + p.add_argument("--limit", type=int, default=None, + help="Cap number of instances sampled. Overrides --stage size " + "for quick checks (e.g. --limit 1).") p.add_argument("--results", type=Path, default=DEFAULT_RESULTS) p.add_argument("--trajectories", type=Path, default=DEFAULT_CACHE_DIR / "trajectories") p.add_argument("--model", default="anthropic/claude-sonnet-4-5", @@ -550,7 +553,10 @@ def main(argv: list[str] | None = None) -> int: from bench.metrics import append_jsonl insts = sample_instances(load_instances(), stage=args.stage) - print(f"[swe-bench] stage={args.stage} sampling {len(insts)} instances") + if args.limit is not None: + insts = insts[: args.limit] + print(f"[swe-bench] stage={args.stage} running {len(insts)} instances " + f"x {len(configs)} configs = {len(insts) * len(configs)} trajectories") for inst in insts: for cfg in configs: # Fresh worktree per (instance, config) to avoid cross-talk. From 13ac345a1e05c606134e9db6fa6d12e00fe6cdc8 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 12:10:26 +0300 Subject: [PATCH 24/63] bench: force tool usage in per-config instance template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke #2 confirmed that even with cg/lsp shims on PATH, indexed repos, REPO_NAME set, and explicit "use cg/lsp first" framing in the system preamble, Claude Opus 4.5 ignored the differentiating tools and fell straight back to grep/sed/cat. The 3-way comparison was real but uninformative: tool choice was identical across configs. This commit adds two new instance templates (INSTANCE_TEMPLATE_LSP and INSTANCE_TEMPLATE_CODE_GRAPH) that embed a 'Required workflow.' block directly in the task description — the first thing the model sees each turn. Selection via load_instance_template(config); baseline keeps the original template. Smoke #3 result: lsp track now invokes 'lsp' 3x, code_graph track invokes 'cg' 5x (including cg auto-complete returning the exact buggy function with line numbers + docstring). The structured-navigation tools are finally exercised, so token deltas measured against baseline are now meaningful signal rather than noise. n=1 finding: both lsp (+128%) and code_graph (+85%) use MORE tokens than baseline on this instance. Bigger preambles + verbose JSON tool replies + occasional retries (cg find-symbol exact-match bug) outweigh any savings. Headline run should scale n or pivot to a function-calling harness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 68 +++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 86b8cbfe..3689c0aa 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -97,6 +97,72 @@ class Task: """ +# The lsp / code_graph configs use a sharper template that mandates an +# initial tool call. Smoke #2 showed Claude reads the system preamble's +# "use cg/lsp first" guidance and then ignores it; embedding the +# requirement in the per-instance task description is more obtrusive. + +INSTANCE_TEMPLATE_LSP = """\ +You are working in the repository at {{cwd}}. + +The task to solve: + +{{task}} + +**Required workflow.** Before reading or editing any file, your first +two bash commands MUST be: + +1. `grep -rn "" --include='*.py' .` + to locate a `file:line` for jedi to anchor on. +2. `lsp goto-definition --file --line --col ` to + resolve the true definition. + +Then use `lsp find-references` whenever you would have used a recursive +grep, and `lsp document-symbols` whenever you would have run a textual +outline pass. Reach for plain grep/sed/cat only after you've exhausted +the LSP for navigation. + +When you believe the task is complete, finish your turn with a final +message that contains a unified diff of your changes inside a fenced +``` block, then exit. Do not commit; the harness reads the diff via +`git diff`. +""" + +INSTANCE_TEMPLATE_CODE_GRAPH = """\ +You are working in the repository at {{cwd}}. +The code-graph service has already indexed this repository under the +name `$REPO_NAME` (use the env var literally). + +The task to solve: + +{{task}} + +**Required workflow.** Before reading or editing any file, your first +bash command MUST be: + + `cg find-symbol --repo "$REPO_NAME" --name ` + +then use `cg get-neighbors --repo "$REPO_NAME" --ids ` to expand +relationships before doing any textual search. After every file edit, +run `cg note-edit --repo "$REPO_NAME" --path ` so subsequent +graph queries reflect your change. Reach for grep/sed/cat only for +content reading after `cg` has located the right place. + +When you believe the task is complete, finish your turn with a final +message that contains a unified diff of your changes inside a fenced +``` block, then exit. Do not commit; the harness reads the diff via +`git diff`. +""" + + +def load_instance_template(config: str) -> str: + if config == "lsp": + return INSTANCE_TEMPLATE_LSP + if config == "code_graph": + return INSTANCE_TEMPLATE_CODE_GRAPH + return INSTANCE_TEMPLATE + + def load_preamble(config: str) -> str: """Read the per-config system preamble; fall back to a generic stub.""" path = TOOLS_DIR / config / "system_preamble.md" @@ -310,7 +376,7 @@ def run_task( model, env, system_template=preamble, - instance_template=INSTANCE_TEMPLATE, + instance_template=load_instance_template(config), step_limit=step_limit, cost_limit=cost_limit, wall_time_limit_seconds=wall_time_limit_seconds, From b0c9ce9a655dd82d5ae36101cfc69479ba1f7222 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 12:15:45 +0300 Subject: [PATCH 25/63] bench: fix find_symbol exact-match against nested properties.name Smoke #3 revealed cg find-symbol --name returned [] for symbols the graph clearly contained (cg auto-complete --prefix found the same symbol with full file:line+docstring). Root cause: the filter compared item['name'] to the requested name, but the /api/auto_complete payload nests the symbol name under item['properties']['name'] (FalkorDB node properties), so the top-level lookup always returned None and nothing matched. Fix: prefer item['properties']['name'], fall back to item['name'] for flatter shapes the unit tests pass in. Added a regression test that uses the real payload structure. Verified end-to-end against the live FastAPI service: cg find-symbol --repo pytest-dev__pytest-6202__code_graph \ --name getmodpath # -> [{id:2714, labels:[Function], properties:{name,path,doc,...}}] This was the bug that made the smoke #3 code_graph agent burn 3 of 5 cg calls retrying exact-name lookups before falling back to auto-complete. With this fix, an agent doing the natural workflow (find-symbol -> get-neighbors -> note-edit) should land far fewer wasted calls. Also: norecursedirs in [tool.pytest.ini_options] to keep pytest from walking into per-instance bench worktrees that ship their own pytest sources (was breaking host pytest's AST rewriter on import). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_adapter.py | 17 +++++++++++++---- pyproject.toml | 4 ++++ tests/test_bench_code_graph_adapter.py | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/bench/agents/code_graph_adapter.py b/bench/agents/code_graph_adapter.py index f3e6e633..96f5cfdf 100644 --- a/bench/agents/code_graph_adapter.py +++ b/bench/agents/code_graph_adapter.py @@ -102,15 +102,24 @@ def find_symbol(self, repo: str, name: str) -> list[dict[str, Any]]: auto_complete returns prefix matches; the agent often wants exact matches. Doing this client-side keeps the FastAPI surface untouched. + + The auto_complete payload nests the symbol name under + `item["properties"]["name"]` (FalkorDB node properties), so we look + there first and only fall back to a top-level `name` for older / + flatter shapes the tests may pass in. """ payload = self.auto_complete(repo, name) results = payload.get("completions") or payload.get("results") or payload if isinstance(results, dict): results = results.get("items", []) - return [ - item for item in (results or []) - if isinstance(item, dict) and item.get("name") == name - ] + out: list[dict[str, Any]] = [] + for item in (results or []): + if not isinstance(item, dict): + continue + props = item.get("properties") if isinstance(item.get("properties"), dict) else {} + if props.get("name") == name or item.get("name") == name: + out.append(item) + return out def note_edit(self, repo: str, path: str) -> dict[str, Any]: """Tell code-graph the agent just edited `path`; trigger an diff --git a/pyproject.toml b/pyproject.toml index cd840469..6f007651 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,10 @@ bench = [ markers = [ "slow: marks tests that spawn external subprocesses (LSP servers, FalkorDB, etc.); skip with -m 'not slow'", ] +# Keep pytest from walking into per-instance bench worktrees that contain +# their own copies of pytest source — collecting from those breaks the +# host pytest's AST rewriter. +norecursedirs = ["bench/cache", ".venv", "node_modules", "build", "dist", "*.egg-info"] [tool.setuptools.packages.find] where = ["."] diff --git a/tests/test_bench_code_graph_adapter.py b/tests/test_bench_code_graph_adapter.py index 192695ed..6b5ee4a1 100644 --- a/tests/test_bench_code_graph_adapter.py +++ b/tests/test_bench_code_graph_adapter.py @@ -80,6 +80,26 @@ def test_find_symbol_filters_for_exact_match(): assert all(item["name"] == "Foo" for item in out) +def test_find_symbol_reads_nested_properties_name(): + # Regression: the real /api/auto_complete payload nests `name` under + # `properties` (FalkorDB node properties). Before this fix every + # exact-name lookup returned [], so the agent fell back to bash grep. + resp = httpx.Response( + 200, + json={"completions": [ + {"id": 1, "labels": ["Function"], + "properties": {"name": "FooBar", "path": "/a.py"}}, + {"id": 2, "labels": ["Function"], + "properties": {"name": "Foo", "path": "/b.py"}}, + {"id": 3, "labels": ["Function"], + "properties": {"name": "Foo", "path": "/c.py"}}, + ]}, + ) + with _client_with({"POST /api/auto_complete": resp}) as c: + out = c.find_symbol("r", "Foo") + assert [item["id"] for item in out] == [2, 3] + + def test_note_edit_calls_analyze_folder_and_reports_path(): resp = httpx.Response(200, json={"status": "ok"}) with _client_with({"POST /api/analyze_folder": resp}) as c: From 921dccdacff4f987bfa86674d77a44d1926b604b Mon Sep 17 00:00:00 2001 From: Dvir Dukhan Date: Wed, 27 May 2026 12:43:52 +0300 Subject: [PATCH 26/63] feat(graph): per-branch graph identity (T17 #651) Refactor FalkorDB graph naming so each (project, branch) pair gets its own graph: 'code:{project}:{branch}'. This lets concurrent agents working on different branches of the same repo index in parallel without overwriting each other. Changes: - api/graph.py: add DEFAULT_BRANCH, compose_graph_name(), parse_graph_name(); Graph and AsyncGraphQuery constructors now accept (name, branch=None); Graph.from_raw_name() classmethod for internal callers that need to bypass composition (e.g. clone()); get_repos()/async_get_repos() now return {project, branch, graph} dicts. - api/info.py: branch-aware Redis hash keys ('{repo}:{branch}_info'); reads fall back to legacy '{repo}_info' for un-migrated graphs. - api/git_utils: GitRepoName() and switch_commit() thread branch through; LegacyGitRepoName() retained for the migration helper. - api/project.py: detect_branch() via 'git rev-parse --abbrev-ref HEAD'; Project.__init__ / from_git_repository / from_local_repository accept branch. - api/index.py: all Pydantic request models gain 'branch: Optional[str]'; endpoints thread it into AsyncGraphQuery + info functions; responses include 'branch'. - api/cli.py: --branch flag on index / index-repo / search / neighbors / paths / info; new 'cgraph migrate' command. - api/migrations/per_branch.py (NEW): idempotent migration that renames legacy '' graphs to 'code::_default', '{}_info' Redis keys to '{}:_default_info', and '{}_git' graphs to '{}:_default_git'. Supports --dry-run. Tests: - tests/test_per_branch_graphs.py (NEW): 24 unit tests covering compose/parse helpers, Graph constructor branch awareness, AsyncGraphQuery, info-key shape, GitRepoName shape, and migration idempotency (with mocked FalkorDB). - tests/test_async_graph.py, tests/test_cli.py, tests/endpoints/test_list_repos.py: updated assertions for the new dict return shape from get_repos / async_get_repos. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/source_analyzer.py | 9 +- api/auto_complete.py | 10 +- api/cli.py | 86 ++++++++-- api/code_coverage/lcov/lcov.py | 6 +- api/git_utils/git_utils.py | 45 +++-- api/graph.py | 174 ++++++++++++++++++-- api/index.py | 67 ++++---- api/info.py | 72 ++++++-- api/migrations/__init__.py | 4 + api/migrations/per_branch.py | 151 +++++++++++++++++ api/project.py | 54 ++++-- pyproject.toml | 6 + tests/endpoints/test_list_repos.py | 4 +- tests/test_async_graph.py | 7 +- tests/test_cli.py | 4 +- tests/test_per_branch_graphs.py | 256 +++++++++++++++++++++++++++++ uv.lock | 35 ++++ 17 files changed, 872 insertions(+), 118 deletions(-) create mode 100644 api/migrations/__init__.py create mode 100644 api/migrations/per_branch.py create mode 100644 tests/test_per_branch_graphs.py diff --git a/api/analyzers/source_analyzer.py b/api/analyzers/source_analyzer.py index 9046abcf..e500caea 100644 --- a/api/analyzers/source_analyzer.py +++ b/api/analyzers/source_analyzer.py @@ -208,21 +208,26 @@ def analyze_local_folder(self, path: str, g: Graph, ignore: Optional[list[str]] logging.info("Done analyzing path") - def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None) -> Graph: + def analyze_local_repository(self, path: str, ignore: Optional[list[str]] = None, branch: Optional[str] = None) -> Graph: """ Analyze a local Git repository. Args: path (str): Path to a local git repository ignore (List(str)): List of paths to skip + branch (Optional[str]): Branch name. Auto-detected from the + checkout when ``None``. """ if ignore is None: ignore = [] from pygit2.repository import Repository + from ..project import detect_branch proj_name = Path(path).name - graph = Graph(proj_name) + if branch is None: + branch = detect_branch(Path(path)) + graph = Graph(proj_name, branch=branch) self.analyze_local_folder(path, graph, ignore) # Save processed commit hash to the DB diff --git a/api/auto_complete.py b/api/auto_complete.py index deebc827..8c69bc8b 100644 --- a/api/auto_complete.py +++ b/api/auto_complete.py @@ -1,15 +1,17 @@ +from typing import Optional + from .graph import Graph, AsyncGraphQuery -def prefix_search(repo: str, prefix: str) -> str: +def prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> str: """ Returns a list of all entities in the repository that start with the given prefix. """ - g = Graph(repo) + g = Graph(repo, branch=branch) return g.prefix_search(prefix) -async def async_prefix_search(repo: str, prefix: str) -> list: +async def async_prefix_search(repo: str, prefix: str, branch: Optional[str] = None) -> list: """Async version of prefix_search using AsyncGraphQuery.""" - g = AsyncGraphQuery(repo) + g = AsyncGraphQuery(repo, branch=branch) try: return await g.prefix_search(prefix) finally: diff --git a/api/cli.py b/api/cli.py index 960f8f39..bf6d9d46 100644 --- a/api/cli.py +++ b/api/cli.py @@ -172,6 +172,9 @@ def index( repo: Optional[str] = typer.Option( None, "--repo", help="Graph name (defaults to folder name)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch to associate with this index (auto-detected from git checkout when omitted; '_default' for non-git paths)" + ), ) -> None: """Index a local folder into the knowledge graph.""" from .project import Project @@ -204,14 +207,14 @@ def index( _stderr(f"Indexing {folder} as '{name}'…") try: - project = Project(name, folder, url) + project = Project(name, folder, url, branch=branch) graph = project.analyze_sources(ignore=list(ignore) if ignore else []) stats = graph.stats() except Exception as e: _json_error(str(e)) - _stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges") - _json_out({"status": "ok", "repo": name, **stats}) + _stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges (branch={project.branch})") + _json_out({"status": "ok", "repo": name, "branch": project.branch, **stats}) # ── index-repo ───────────────────────────────────────────────────────── @@ -223,6 +226,9 @@ def index_repo( ignore: Optional[List[str]] = typer.Option( None, "--ignore", help="Directories to ignore (repeatable)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch to associate with this index (auto-detected from the cloned checkout when omitted)" + ), ) -> None: """Clone a git repository and index it into the knowledge graph.""" from .project import Project @@ -233,14 +239,14 @@ def index_repo( import io import contextlib with contextlib.redirect_stdout(io.StringIO()): - project = Project.from_git_repository(url) + project = Project.from_git_repository(url, branch=branch) graph = project.analyze_sources(ignore=list(ignore) if ignore else []) stats = graph.stats() except Exception as e: _json_error(str(e)) - _stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges") - _json_out({"status": "ok", "repo": project.name, **stats}) + _stderr(f"Done — {stats['node_count']} nodes, {stats['edge_count']} edges (branch={project.branch})") + _json_out({"status": "ok", "repo": project.name, "branch": project.branch, **stats}) # ── list ─────────────────────────────────────────────────────────────── @@ -248,7 +254,7 @@ def index_repo( @app.command("list") def list_repos() -> None: - """List all indexed repositories.""" + """List all indexed (project, branch) pairs.""" from .graph import get_repos try: @@ -259,6 +265,30 @@ def list_repos() -> None: _json_out({"repos": repos}) +# ── migrate ──────────────────────────────────────────────────────────── + + +@app.command("migrate") +def migrate( + dry_run: bool = typer.Option(False, "--dry-run", help="Print actions without performing them"), +) -> None: + """Promote legacy (pre-T17) graphs and Redis keys into the per-branch namespace. + + Renames each legacy ```` graph to ``code::_default``, + each ``{project}_info`` Redis key to ``{project}:_default_info``, and + each ``{project}_git`` graph to ``{project}:_default_git``. Idempotent. + """ + + from .migrations.per_branch import run_migration + + try: + result = run_migration(dry_run=dry_run) + except Exception as e: + _json_error(str(e)) + + _json_out(result) + + # ── search ───────────────────────────────────────────────────────────── @@ -268,18 +298,24 @@ def search( repo: Optional[str] = typer.Option( None, "--repo", help="Repository name (defaults to CWD name)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)" + ), ) -> None: """Search for entities by prefix (full-text search).""" from .graph import Graph + from .project import detect_branch name = _default_repo(repo) + if branch is None: + branch = detect_branch(Path.cwd()) try: - g = Graph(name) + g = Graph(name, branch=branch) results = g.prefix_search(query) except Exception as e: _json_error(str(e)) - _json_out({"repo": name, "results": results}) + _json_out({"repo": name, "branch": branch, "results": results}) # ── neighbors ────────────────────────────────────────────────────────── @@ -297,18 +333,24 @@ def neighbors( label: Optional[str] = typer.Option( None, "--label", help="Filter by destination label (e.g. Function, Class)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)" + ), ) -> None: """Get neighboring entities of the given node(s).""" from .graph import Graph + from .project import detect_branch name = _default_repo(repo) + if branch is None: + branch = detect_branch(Path.cwd()) try: - g = Graph(name) + g = Graph(name, branch=branch) result = g.get_neighbors(node_ids, rel=rel, lbl=label) except Exception as e: _json_error(str(e)) - _json_out({"repo": name, **result}) + _json_out({"repo": name, "branch": branch, **result}) # ── paths ────────────────────────────────────────────────────────────── @@ -321,18 +363,24 @@ def paths( repo: Optional[str] = typer.Option( None, "--repo", help="Repository name (defaults to CWD name)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)" + ), ) -> None: """Find call-chain paths between two nodes.""" from .graph import Graph + from .project import detect_branch name = _default_repo(repo) + if branch is None: + branch = detect_branch(Path.cwd()) try: - g = Graph(name) + g = Graph(name, branch=branch) result = g.find_paths(src, dest) except Exception as e: _json_error(str(e)) - _json_out({"repo": name, "paths": result}) + _json_out({"repo": name, "branch": branch, "paths": result}) # ── info ─────────────────────────────────────────────────────────────── @@ -343,20 +391,26 @@ def info( repo: Optional[str] = typer.Option( None, "--repo", help="Repository name (defaults to CWD name)" ), + branch: Optional[str] = typer.Option( + None, "--branch", help="Branch (auto-detected from CWD; '_default' for non-git paths)" + ), ) -> None: """Show repository statistics and metadata.""" from .graph import Graph from .info import get_repo_info + from .project import detect_branch name = _default_repo(repo) + if branch is None: + branch = detect_branch(Path.cwd()) try: - g = Graph(name) + g = Graph(name, branch=branch) stats = g.stats() - metadata = get_repo_info(name) or {} + metadata = get_repo_info(name, branch) or {} except Exception as e: _json_error(str(e)) - _json_out({"repo": name, **stats, "metadata": metadata}) + _json_out({"repo": name, "branch": branch, **stats, "metadata": metadata}) if __name__ == "__main__": diff --git a/api/code_coverage/lcov/lcov.py b/api/code_coverage/lcov/lcov.py index 734f285d..94daae31 100644 --- a/api/code_coverage/lcov/lcov.py +++ b/api/code_coverage/lcov/lcov.py @@ -1,5 +1,7 @@ import os import sys +from typing import Optional + from ...graph import Graph def lcovparse(content): @@ -124,7 +126,7 @@ def _line(l, report): else: sys.stdout.write("Unknown method name %s" % method) -def process_lcov(repo: str, lcov_file: str) -> None: +def process_lcov(repo: str, lcov_file: str, branch: Optional[str] = None) -> None: # create report from coverage lcov file with open(lcov_file, "r") as file: content = file.read() # Reads the entire file as a single string @@ -134,7 +136,7 @@ def process_lcov(repo: str, lcov_file: str) -> None: # SF:/__w/FalkorDB/FalkorDB/src/algorithms/detect_cycle.c prefix = "/__w/FalkorDB/FalkorDB/" # prefix to remove - g = Graph(repo) + g = Graph(repo, branch=branch) #--------------------------------------------------------------------------- # Process report diff --git a/api/git_utils/git_utils.py b/api/git_utils/git_utils.py index 0c723b03..e9cbad19 100644 --- a/api/git_utils/git_utils.py +++ b/api/git_utils/git_utils.py @@ -14,8 +14,21 @@ # Configure logging logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s') -def GitRepoName(repo_name): - """ Returns the git repository name """ +def GitRepoName(repo_name, branch=None): + """ Returns the git transitions graph key for ``(repo_name, branch)``. + + Format: ``{repo_name}:{branch}_git``. Hash-tag stays on ``repo_name`` + so the git-graph key lives on the same FalkorDB cluster slot as its + sibling code graph and ``*_info`` Redis hash. + """ + from ..graph import DEFAULT_BRANCH + if branch is None or branch == "": + branch = DEFAULT_BRANCH + return "{" + repo_name + "}" + ":" + branch + "_git" + + +def LegacyGitRepoName(repo_name): + """Pre-T17 git graph key shape — kept for the migration helper.""" return "{" + repo_name + "}_git" def is_ignored(file_path: str, ignore_list: List[str]) -> bool: @@ -70,7 +83,7 @@ def classify_changes( return added, deleted, modified # build a graph capturing the git commit history -def build_commit_graph(path: str, analyzer: SourceAnalyzer, repo_name: str, ignore_list: Optional[List[str]] = None) -> GitGraph: +def build_commit_graph(path: str, analyzer: SourceAnalyzer, repo_name: str, ignore_list: Optional[List[str]] = None, branch: Optional[str] = None) -> GitGraph: """ Builds a graph representation of the git commit history. @@ -78,6 +91,7 @@ def build_commit_graph(path: str, analyzer: SourceAnalyzer, repo_name: str, igno path (str): Path to the git repository. repo_name (str): Name of the repository. ignore_list (List[str], optional): List of file patterns to ignore. + branch (Optional[str]): Branch name. Defaults to ``_default``. Returns: GitGraph: Graph object representing the commit history. @@ -86,13 +100,15 @@ def build_commit_graph(path: str, analyzer: SourceAnalyzer, repo_name: str, igno if ignore_list is None: ignore_list = [] - # Copy the graph into a temporary graph - logging.info("Cloning source graph %s -> %s_tmp", repo_name, repo_name) - # Will be deleted at the end of this function - g = Graph(repo_name).clone(repo_name + "_tmp") + # Copy the graph into a temporary graph (sibling key with `_tmp` suffix on + # the branch component so the clone lands on the same cluster slot). + source = Graph(repo_name, branch=branch) + tmp_name = source.name + "_tmp" + logging.info("Cloning source graph %s -> %s", source.name, tmp_name) + g = source.clone(tmp_name) g.enable_backlog() - git_graph = GitGraph(GitRepoName(repo_name)) + git_graph = GitGraph(GitRepoName(repo_name, branch)) supported_types = analyzer.supported_types() # Initialize with the current commit @@ -252,12 +268,12 @@ def build_commit_graph(path: str, analyzer: SourceAnalyzer, repo_name: str, igno # Delete temporaty graph g.disable_backlog() - logging.debug(f"Deleting temporary graph {repo_name + '_tmp'}") + logging.debug(f"Deleting temporary graph {g.name}") g.delete() return git_graph -def switch_commit(repo: str, to: str): +def switch_commit(repo: str, to: str, branch: Optional[str] = None): """ Switches the state of a graph repository from its current commit to the given commit. @@ -268,6 +284,7 @@ def switch_commit(repo: str, to: str): Args: repo (str): The name of the graph repository to switch commits. to (str): The target commit hash to switch the graph to. + branch (Optional[str]): The branch. Defaults to ``_default``. """ # Validate input arguments @@ -280,11 +297,11 @@ def switch_commit(repo: str, to: str): logging.info(f"Switching to commit: {to}") # Initialize the graph and GitGraph objects - g = Graph(repo) - git_graph = GitGraph(GitRepoName(repo)) + g = Graph(repo, branch=branch) + git_graph = GitGraph(GitRepoName(repo, branch)) # Get the current commit hash of the graph - current_hash = get_repo_commit(repo) + current_hash = get_repo_commit(repo, branch) logging.info(f"Current graph commit: {current_hash}") if current_hash == to: @@ -329,5 +346,5 @@ def switch_commit(repo: str, to: str): g.rerun_query(_q, _p) # Update the graph's commit to the new target commit - set_repo_commit(repo, to) + set_repo_commit(repo, to, branch) logging.info(f"Graph commit updated to {to}") diff --git a/api/graph.py b/api/graph.py index eda72e63..5ec1863c 100644 --- a/api/graph.py +++ b/api/graph.py @@ -1,4 +1,5 @@ import os +import re import time from .entities import * from typing import Optional @@ -10,6 +11,56 @@ logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s') + +# --------------------------------------------------------------------------- +# Branch-aware graph naming (T17) +# +# Each indexed (project, branch) pair gets its own FalkorDB graph so that +# concurrent agents indexing the same repo on different branches do not +# overwrite each other. The format is:: +# +# code:{project_name}:{branch} +# +# When ``branch`` is omitted it defaults to ``_default`` — that is also the +# name the one-shot migration uses when promoting legacy ``{project_name}`` +# graphs into the new namespace. +# --------------------------------------------------------------------------- + +DEFAULT_BRANCH = "_default" +_GRAPH_NAME_RE = re.compile(r"^code:(?P[^:]+):(?P.+)$") + + +def compose_graph_name(project_name: str, branch: Optional[str] = None) -> str: + """Compose the FalkorDB graph name for a (project, branch) pair. + + Args: + project_name: The repository / project name (typically the + directory basename). + branch: Branch name. ``None`` is treated as :data:`DEFAULT_BRANCH`. + + Returns: + ``"code:{project_name}:{branch}"``. + """ + + if branch is None or branch == "": + branch = DEFAULT_BRANCH + return f"code:{project_name}:{branch}" + + +def parse_graph_name(graph_name: str) -> Optional[tuple[str, str]]: + """Inverse of :func:`compose_graph_name`. + + Returns ``(project, branch)`` if ``graph_name`` follows the new + ``code:{project}:{branch}`` format, otherwise ``None`` so callers can + treat it as a legacy / unrelated graph. + """ + + m = _GRAPH_NAME_RE.match(graph_name) + if not m: + return None + return m.group("project"), m.group("branch") + + def graph_exists(name: str): db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'), port=os.getenv('FALKORDB_PORT', 6379), @@ -18,9 +69,21 @@ def graph_exists(name: str): return name in db.list_graphs() -def get_repos() -> list[str]: + +def _is_internal_suffix(graph_name: str) -> bool: + """Internal helper graph suffixes that should never be listed as repos.""" + return graph_name.endswith('_git') or graph_name.endswith('_schema') or graph_name.endswith('_tmp') + + +def get_repos() -> list[dict]: """ - List processed repositories + List processed (project, branch) pairs. + + Returns a list of ``{"project": ..., "branch": ..., "graph": ...}`` + dicts for every graph that matches the new ``code:{project}:{branch}`` + format. Legacy graphs (created before T17) are returned with + ``branch == DEFAULT_BRANCH`` so callers can keep treating them as a + single graph until the migration is run. """ db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'), @@ -28,22 +91,52 @@ def get_repos() -> list[str]: username=os.getenv('FALKORDB_USERNAME', None), password=os.getenv('FALKORDB_PASSWORD', None)) - graphs = db.list_graphs() - graphs = [g for g in graphs if not (g.endswith('_git') or g.endswith('_schema'))] - return graphs + repos = [] + for g in db.list_graphs(): + if _is_internal_suffix(g): + continue + parsed = parse_graph_name(g) + if parsed is None: + # Legacy graph (pre-T17): synthesize a virtual entry so it stays + # discoverable until the migration helper promotes it. + repos.append({"project": g, "branch": DEFAULT_BRANCH, "graph": g}) + else: + project, branch = parsed + repos.append({"project": project, "branch": branch, "graph": g}) + return repos class Graph(): """ Represents a connection to a graph database using FalkorDB. + + The underlying graph is named ``code:{project_name}:{branch}`` so that + concurrent agents working on different branches of the same repo do + not corrupt each other's data (see T17, issue #651). + + For backwards compatibility ``name`` may be either a bare project name + (``"falkordb"``) or a fully composed graph name (``"code:falkordb:main"``); + in the former case the composition is performed automatically using + ``branch`` (default :data:`DEFAULT_BRANCH`). """ - def __init__(self, name: str) -> None: - self.name = name + def __init__(self, name: str, branch: Optional[str] = None) -> None: + # Accept either an already-composed graph name or a bare project + # name + branch. ``parse_graph_name`` returns ``None`` for legacy / + # bare names, signalling that we need to compose. + parsed = parse_graph_name(name) + if parsed is not None: + self.project, self.branch = parsed + self.name = name + else: + self.project = name + self.branch = branch if branch is not None else DEFAULT_BRANCH + self.name = compose_graph_name(self.project, self.branch) + self.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'), port=os.getenv('FALKORDB_PORT', 6379), username=os.getenv('FALKORDB_USERNAME', None), password=os.getenv('FALKORDB_PASSWORD', None)) - self.g = self.db.select_graph(name) + self.g = self.db.select_graph(self.name) # Initialize the backlog as disabled by default self.backlog = None @@ -62,9 +155,34 @@ def __init__(self, name: str) -> None: except Exception: pass + @classmethod + def from_raw_name(cls, raw_name: str) -> "Graph": + """Construct a :class:`Graph` from an already-composed (or raw) name. + + Used by :meth:`clone` and the migration helper, where the caller + already knows the final FalkorDB key. Bypasses + :func:`compose_graph_name`. + """ + + obj = cls.__new__(cls) + obj.name = raw_name + parsed = parse_graph_name(raw_name) + if parsed is None: + obj.project = raw_name + obj.branch = DEFAULT_BRANCH + else: + obj.project, obj.branch = parsed + obj.db = FalkorDB(host=os.getenv('FALKORDB_HOST', 'localhost'), + port=os.getenv('FALKORDB_PORT', 6379), + username=os.getenv('FALKORDB_USERNAME', None), + password=os.getenv('FALKORDB_PASSWORD', None)) + obj.g = obj.db.select_graph(raw_name) + obj.backlog = None + return obj + def clone(self, clone: str) -> "Graph": """ - Create a copy of the graph under the name clone + Create a copy of the graph under the name clone (raw FalkorDB key). Returns: a new instance of Graph @@ -81,7 +199,7 @@ def clone(self, clone: str) -> "Graph": # TODO: add a waiting limit time.sleep(1) - return Graph(clone) + return Graph.from_raw_name(clone) def delete(self) -> None: @@ -639,12 +757,24 @@ async def async_graph_exists(name: str) -> bool: await db.aclose() -async def async_get_repos() -> list[str]: - """List processed repositories (async version).""" +async def async_get_repos() -> list[dict]: + """List processed (project, branch) pairs (async version). + + Mirrors :func:`get_repos`; see that function for the return shape. + """ db = _async_db() try: - graphs = await db.list_graphs() - return [g for g in graphs if not (g.endswith('_git') or g.endswith('_schema'))] + repos = [] + for g in await db.list_graphs(): + if _is_internal_suffix(g): + continue + parsed = parse_graph_name(g) + if parsed is None: + repos.append({"project": g, "branch": DEFAULT_BRANCH, "graph": g}) + else: + project, branch = parsed + repos.append({"project": project, "branch": branch, "graph": g}) + return repos finally: await db.aclose() @@ -654,12 +784,22 @@ class AsyncGraphQuery: Uses falkordb.asyncio under the hood. No index creation or backlog — indexes already exist from the sync Graph used during analysis. + + Accepts either a bare project name + branch or a fully composed graph + name (``code:{project}:{branch}``); see :class:`Graph` for details. """ - def __init__(self, name: str) -> None: - self.name = name + def __init__(self, name: str, branch: Optional[str] = None) -> None: + parsed = parse_graph_name(name) + if parsed is not None: + self.project, self.branch = parsed + self.name = name + else: + self.project = name + self.branch = branch if branch is not None else DEFAULT_BRANCH + self.name = compose_graph_name(self.project, self.branch) self.db = _async_db() - self.g = self.db.select_graph(name) + self.g = self.db.select_graph(self.name) async def graph_exists(self) -> bool: """Check if this graph exists, reusing the current connection.""" diff --git a/api/index.py b/api/index.py index 38dfb61d..b41023d6 100644 --- a/api/index.py +++ b/api/index.py @@ -3,6 +3,7 @@ import asyncio import logging from pathlib import Path +from typing import Optional from dotenv import load_dotenv from fastapi import Depends, FastAPI, Header, HTTPException, Query @@ -15,7 +16,7 @@ from api.graph import Graph, AsyncGraphQuery, async_get_repos from api.info import async_get_repo_info from api.llm import ask -from api.project import Project +from api.project import Project, detect_branch # Load environment variables from .env file @@ -56,35 +57,43 @@ def token_required(authorization: str | None = Header(None)): class RepoRequest(BaseModel): repo: str + branch: Optional[str] = None class NeighborsRequest(BaseModel): repo: str node_ids: list[int] + branch: Optional[str] = None class AutoCompleteRequest(BaseModel): repo: str prefix: str + branch: Optional[str] = None class FindPathsRequest(BaseModel): repo: str src: int dest: int + branch: Optional[str] = None class ChatRequest(BaseModel): repo: str msg: str + branch: Optional[str] = None class AnalyzeFolderRequest(BaseModel): path: str ignore: list[str] = [] + branch: Optional[str] = None class AnalyzeRepoRequest(BaseModel): repo_url: str ignore: list[str] = [] + branch: Optional[str] = None class SwitchCommitRequest(BaseModel): repo: str commit: str + branch: Optional[str] = None # --------------------------------------------------------------------------- # Application @@ -105,23 +114,23 @@ class SwitchCommitRequest(BaseModel): # --------------------------------------------------------------------------- @app.get('/api/graph_entities') -async def graph_entities(repo: str = Query(None), _=Depends(public_or_auth)): +async def graph_entities(repo: str = Query(None), branch: Optional[str] = Query(None), _=Depends(public_or_auth)): """Fetch sub-graph entities from a given repository.""" if not repo: logging.error("Missing 'repo' parameter in request.") return JSONResponse({"status": "Missing 'repo' parameter"}, status_code=400) - g = AsyncGraphQuery(repo) + g = AsyncGraphQuery(repo, branch=branch) try: if not await g.graph_exists(): - logging.error("Missing project %s", repo) + logging.error("Missing project %s (branch=%s)", repo, g.branch) return JSONResponse({"status": f"Missing project {repo}"}, status_code=400) sub_graph = await g.get_sub_graph(500) - logging.info("Successfully retrieved sub-graph for repo: %s", repo) - return {"status": "success", "entities": sub_graph} + logging.info("Successfully retrieved sub-graph for repo: %s (branch=%s)", repo, g.branch) + return {"status": "success", "branch": g.branch, "entities": sub_graph} except Exception as e: logging.exception("Error retrieving sub-graph for repo '%s': %s", repo, e) @@ -134,26 +143,26 @@ async def graph_entities(repo: str = Query(None), _=Depends(public_or_auth)): async def get_neighbors(data: NeighborsRequest, _=Depends(public_or_auth)): """Get neighbors of a nodes list in the graph.""" - g = AsyncGraphQuery(data.repo) + g = AsyncGraphQuery(data.repo, branch=data.branch) try: if not await g.graph_exists(): - logging.error("Missing project %s", data.repo) + logging.error("Missing project %s (branch=%s)", data.repo, g.branch) return JSONResponse({"status": f"Missing project {data.repo}"}, status_code=400) neighbors = await g.get_neighbors(data.node_ids) finally: await g.close() - logging.info("Successfully retrieved neighbors for node IDs %s in repo '%s'.", - data.node_ids, data.repo) - return {"status": "success", "neighbors": neighbors} + logging.info("Successfully retrieved neighbors for node IDs %s in repo '%s' (branch=%s).", + data.node_ids, data.repo, g.branch) + return {"status": "success", "branch": g.branch, "neighbors": neighbors} @app.post('/api/auto_complete') async def auto_complete(data: AutoCompleteRequest, _=Depends(public_or_auth)): """Process auto-completion requests for a repository based on a prefix.""" - g = AsyncGraphQuery(data.repo) + g = AsyncGraphQuery(data.repo, branch=data.branch) try: if not await g.graph_exists(): return JSONResponse({"status": f"Missing project {data.repo}"}, status_code=400) @@ -161,12 +170,12 @@ async def auto_complete(data: AutoCompleteRequest, _=Depends(public_or_auth)): completions = await g.prefix_search(data.prefix) finally: await g.close() - return {"status": "success", "completions": completions} + return {"status": "success", "branch": g.branch, "completions": completions} @app.get('/api/list_repos') async def list_repos(_=Depends(public_or_auth)): - """List all available repositories.""" + """List all available repositories (returns (project, branch) pairs).""" repos = await async_get_repos() return {"status": "success", "repositories": repos} @@ -176,7 +185,7 @@ async def list_repos(_=Depends(public_or_auth)): async def repo_info(data: RepoRequest, _=Depends(public_or_auth)): """Retrieve information about a specific repository.""" - g = AsyncGraphQuery(data.repo) + g = AsyncGraphQuery(data.repo, branch=data.branch) try: if not await g.graph_exists(): return JSONResponse({"status": f'Missing repository "{data.repo}"'}, status_code=400) @@ -184,29 +193,29 @@ async def repo_info(data: RepoRequest, _=Depends(public_or_auth)): stats = await g.stats() finally: await g.close() - info = await async_get_repo_info(data.repo) + info = await async_get_repo_info(data.repo, data.branch) if info is None: return JSONResponse({"status": f'Missing repository "{data.repo}"'}, status_code=400) stats |= info - return {"status": "success", "info": stats} + return {"status": "success", "branch": g.branch, "info": stats} @app.post('/api/find_paths') async def find_paths(data: FindPathsRequest, _=Depends(public_or_auth)): """Find all paths between a source and destination node in the graph.""" - g = AsyncGraphQuery(data.repo) + g = AsyncGraphQuery(data.repo, branch=data.branch) try: if not await g.graph_exists(): - logging.error("Missing project %s", data.repo) + logging.error("Missing project %s (branch=%s)", data.repo, g.branch) return JSONResponse({"status": f"Missing project {data.repo}"}, status_code=400) paths = await g.find_paths(data.src, data.dest) finally: await g.close() - return {"status": "success", "paths": paths} + return {"status": "success", "branch": g.branch, "paths": paths} @app.post('/api/chat') @@ -241,33 +250,35 @@ async def analyze_folder(data: AnalyzeFolderRequest, _=Depends(token_required)): status_code=400) proj_name = resolved_path.name + branch = data.branch if data.branch is not None else detect_branch(resolved_path) def _analyze(): - g = Graph(proj_name) + g = Graph(proj_name, branch=branch) analyzer = SourceAnalyzer() analyzer.analyze_local_folder(str(resolved_path), g, data.ignore) loop = asyncio.get_running_loop() await loop.run_in_executor(None, _analyze) - return {"status": "success", "project": proj_name} + return {"status": "success", "project": proj_name, "branch": branch} @app.post('/api/analyze_repo') async def analyze_repo(data: AnalyzeRepoRequest, _=Depends(token_required)): """Analyze a GitHub repository. Always requires a valid token.""" - logger.debug('Received repo_url: %s', data.repo_url) + logger.debug('Received repo_url: %s branch: %s', data.repo_url, data.branch) def _analyze(): - proj = Project.from_git_repository(data.repo_url) + proj = Project.from_git_repository(data.repo_url, branch=data.branch) proj.analyze_sources(data.ignore) proj.process_git_history(data.ignore) + return proj.branch loop = asyncio.get_running_loop() - await loop.run_in_executor(None, _analyze) + resolved_branch = await loop.run_in_executor(None, _analyze) - return {"status": "success"} + return {"status": "success", "branch": resolved_branch} @app.post('/api/switch_commit') @@ -275,7 +286,7 @@ async def switch_commit(data: SwitchCommitRequest, _=Depends(token_required)): """Switch a repository to a specific commit. Always requires a valid token.""" loop = asyncio.get_running_loop() - await loop.run_in_executor(None, git_utils.switch_commit, data.repo, data.commit) + await loop.run_in_executor(None, git_utils.switch_commit, data.repo, data.commit, data.branch) return {"status": "success"} @@ -283,7 +294,7 @@ async def switch_commit(data: SwitchCommitRequest, _=Depends(token_required)): async def list_commits(data: RepoRequest, _=Depends(public_or_auth)): """List all commits of a specified repository.""" - git_graph = AsyncGitGraph(git_utils.GitRepoName(data.repo)) + git_graph = AsyncGitGraph(git_utils.GitRepoName(data.repo, data.branch)) try: commits = await git_graph.list_commits() finally: diff --git a/api/info.py b/api/info.py index b1d9ea7e..3b41d042 100644 --- a/api/info.py +++ b/api/info.py @@ -4,10 +4,31 @@ import logging from typing import Optional, Dict +from .graph import DEFAULT_BRANCH + # Configure logging logging.basicConfig(level=logging.INFO) -def _repo_info_key(repo_name: str) -> str: + +def _normalize_branch(branch: Optional[str]) -> str: + if branch is None or branch == "": + return DEFAULT_BRANCH + return branch + + +def _repo_info_key(repo_name: str, branch: Optional[str] = None) -> str: + """Compose the Redis hash key holding ``(repo, branch)`` metadata. + + The curly-brace hash-tag stays on ``repo_name`` so per-branch metadata + keys land on the same FalkorDB cluster slot as the equivalent graph + keys (e.g. ``{repo}:{branch}_git``). + """ + branch = _normalize_branch(branch) + return f"{{{repo_name}}}:{branch}_info" + + +def _legacy_repo_info_key(repo_name: str) -> str: + """Pre-T17 key shape, retained for the migration helper / fallback reads.""" return f"{{{repo_name}}}_info" def get_redis_connection() -> redis.Redis: @@ -30,12 +51,12 @@ def get_redis_connection() -> redis.Redis: raise -def set_repo_commit(repo_name: str, commit_hash: str) -> None: - """Save processed commit hash to the DB""" +def set_repo_commit(repo_name: str, commit_hash: str, branch: Optional[str] = None) -> None: + """Save processed commit hash to the DB for ``(repo_name, branch)``.""" try: r = get_redis_connection() - key = _repo_info_key(repo_name) # Safely format the key + key = _repo_info_key(repo_name, branch) # Safely format the key # Save the repository URL r.hset(key, 'commit', commit_hash) @@ -46,15 +67,19 @@ def set_repo_commit(repo_name: str, commit_hash: str) -> None: raise -def get_repo_commit(repo_name: str) -> str: - """Get the current commit the repo is at""" +def get_repo_commit(repo_name: str, branch: Optional[str] = None) -> str: + """Get the current commit the repo is at for ``(repo_name, branch)``.""" try: r = get_redis_connection() - key = _repo_info_key(repo_name) + key = _repo_info_key(repo_name, branch) # Retrieve all information about the repository commit_hash = r.hget(key, "commit") + if not commit_hash: + # Fall back to the legacy single-key shape, so reads against + # un-migrated graphs still succeed. + commit_hash = r.hget(_legacy_repo_info_key(repo_name), "commit") if not commit_hash: logging.warning(f"Failed to retrieve {repo_name} current commit hash") return None @@ -67,18 +92,20 @@ def get_repo_commit(repo_name: str) -> str: raise -def save_repo_info(repo_name: str, repo_url: str) -> None: +def save_repo_info(repo_name: str, repo_url: str, branch: Optional[str] = None) -> None: """ - Saves repository information (URL) to Redis under a hash named {repo_name}_info. + Saves repository information (URL) to Redis under a hash named + ``{repo_name}:{branch}_info``. Args: repo_name (str): The name of the repository. repo_url (str): The URL of the repository. + branch (Optional[str]): The branch. Defaults to ``_default``. """ try: r = get_redis_connection() - key = _repo_info_key(repo_name) + key = _repo_info_key(repo_name, branch) # Save the repository URL r.hset(key, 'repo_url', repo_url) @@ -88,27 +115,34 @@ def save_repo_info(repo_name: str, repo_url: str) -> None: logging.error(f"Error saving repo info for '{repo_name}': {e}") raise -def get_repo_info(repo_name: str) -> Optional[Dict[str, str]]: +def get_repo_info(repo_name: str, branch: Optional[str] = None) -> Optional[Dict[str, str]]: """ - Retrieves repository information from Redis. + Retrieves repository information from Redis for ``(repo_name, branch)``. + + Falls back to the legacy single-key shape so pre-migration graphs + remain readable. Args: repo_name (str): The name of the repository. + branch (Optional[str]): The branch. Defaults to ``_default``. Returns: - Optional[Dict[str, str]]: A dictionary of repository information, or None if not found. + Optional[Dict[str, str]]: A dictionary of repository information, + or ``None`` if not found. """ try: r = get_redis_connection() - key = _repo_info_key(repo_name) - + key = _repo_info_key(repo_name, branch) + # Retrieve all information about the repository repo_info = r.hgetall(key) + if not repo_info: + repo_info = r.hgetall(_legacy_repo_info_key(repo_name)) if not repo_info: logging.warning(f"No repository info found for {repo_name}") return None - + logging.info(f"Repository info retrieved for {repo_name}") return repo_info @@ -131,12 +165,14 @@ async def async_get_redis_connection() -> aioredis.Redis: ) -async def async_get_repo_info(repo_name: str) -> Optional[Dict[str, str]]: +async def async_get_repo_info(repo_name: str, branch: Optional[str] = None) -> Optional[Dict[str, str]]: try: r = await async_get_redis_connection() try: - key = _repo_info_key(repo_name) + key = _repo_info_key(repo_name, branch) repo_info = await r.hgetall(key) + if not repo_info: + repo_info = await r.hgetall(_legacy_repo_info_key(repo_name)) if not repo_info: logging.warning(f"No repository info found for {repo_name}") return None diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 00000000..e6f73896 --- /dev/null +++ b/api/migrations/__init__.py @@ -0,0 +1,4 @@ +"""One-shot data migrations for code-graph. + +Each migration is idempotent and safe to re-run. +""" diff --git a/api/migrations/per_branch.py b/api/migrations/per_branch.py new file mode 100644 index 00000000..f54f2223 --- /dev/null +++ b/api/migrations/per_branch.py @@ -0,0 +1,151 @@ +"""T17 (#651): promote legacy graphs into the per-branch namespace. + +Before T17 a repo named ``myrepo`` lived at: + + * Graph key: ``myrepo`` + * Info hash: ``{myrepo}_info`` + * Git graph: ``{myrepo}_git`` + +After T17, the new format is: + + * Graph key: ``code:myrepo:_default`` + * Info hash: ``{myrepo}:_default_info`` + * Git graph: ``{myrepo}:_default_git`` + +This module renames every legacy artifact in place. It is safe to run +multiple times — already-migrated graphs are skipped — and exposes a +``--dry-run`` mode via ``cgraph migrate`` for previewing changes. +""" + +from __future__ import annotations + +import logging +import os +from typing import Iterable + +from falkordb import FalkorDB + +from ..graph import ( + DEFAULT_BRANCH, + compose_graph_name, + parse_graph_name, +) +from ..info import _legacy_repo_info_key, _repo_info_key + + +logger = logging.getLogger(__name__) + + +def _connect() -> FalkorDB: + return FalkorDB( + host=os.getenv("FALKORDB_HOST", "localhost"), + port=os.getenv("FALKORDB_PORT", 6379), + username=os.getenv("FALKORDB_USERNAME", None), + password=os.getenv("FALKORDB_PASSWORD", None), + ) + + +def _legacy_graphs(all_graphs: Iterable[str]) -> list[str]: + """Return graphs that look like the pre-T17 single-name shape.""" + + legacy = [] + for name in all_graphs: + # Already migrated? + if parse_graph_name(name) is not None: + continue + # System graphs etc. — also skip the ``_tmp`` mid-migration name and + # the per-repo ``_git`` companion (handled separately, see below). + if name.endswith("_tmp") or name.endswith("_schema") or name.endswith("_git"): + continue + legacy.append(name) + return legacy + + +def _rename_graph(db: FalkorDB, src: str, dst: str, *, dry_run: bool) -> bool: + """Copy ``src`` to ``dst`` and delete ``src``. Returns ``True`` on success. + + Skips with ``False`` (and a warning) when ``dst`` already exists. + """ + + if db.connection.exists(dst): + logger.warning("Target graph %s already exists — skipping rename of %s", dst, src) + return False + if dry_run: + logger.info("[dry-run] would rename graph %s -> %s", src, dst) + return True + g = db.select_graph(src) + g.copy(dst) + g.delete() + logger.info("Renamed graph %s -> %s", src, dst) + return True + + +def _rename_redis_key(db: FalkorDB, src: str, dst: str, *, dry_run: bool) -> bool: + """Rename a Redis hash key. Idempotent — skips when ``dst`` already exists. + + Uses the underlying ``redis-py`` connection on the FalkorDB client so + we don't need a separate Redis connection. + """ + + conn = db.connection + if not conn.exists(src): + return False + if conn.exists(dst): + logger.warning("Target Redis key %s already exists — skipping rename of %s", dst, src) + return False + if dry_run: + logger.info("[dry-run] would rename Redis key %s -> %s", src, dst) + return True + conn.rename(src, dst) + logger.info("Renamed Redis key %s -> %s", src, dst) + return True + + +def run_migration(*, dry_run: bool = False) -> dict: + """Run the per-branch migration once. + + Returns a small summary dict suitable for the CLI to print as JSON:: + + {"status": "ok", "graphs_renamed": int, "info_renamed": int, + "git_renamed": int, "dry_run": bool, "skipped": list[str]} + """ + + db = _connect() + all_graphs = list(db.list_graphs()) + legacy = _legacy_graphs(all_graphs) + + graphs_renamed = 0 + info_renamed = 0 + git_renamed = 0 + skipped: list[str] = [] + + for project in legacy: + # Rename the main code graph itself. + new_graph = compose_graph_name(project, DEFAULT_BRANCH) + if _rename_graph(db, project, new_graph, dry_run=dry_run): + graphs_renamed += 1 + else: + skipped.append(project) + continue + + # Rename the sibling Redis info key, if any. + legacy_info = _legacy_repo_info_key(project) + new_info = _repo_info_key(project, DEFAULT_BRANCH) + if _rename_redis_key(db, legacy_info, new_info, dry_run=dry_run): + info_renamed += 1 + + # Rename the sibling ``{project}_git`` graph, if any. + legacy_git = "{" + project + "}_git" + new_git = "{" + project + "}:" + DEFAULT_BRANCH + "_git" + if legacy_git in all_graphs: + if _rename_graph(db, legacy_git, new_git, dry_run=dry_run): + git_renamed += 1 + + return { + "status": "ok", + "dry_run": dry_run, + "graphs_renamed": graphs_renamed, + "info_renamed": info_renamed, + "git_renamed": git_renamed, + "skipped": skipped, + } diff --git a/api/project.py b/api/project.py index aed5a9e7..2478c7c4 100644 --- a/api/project.py +++ b/api/project.py @@ -6,7 +6,7 @@ from pygit2.repository import Repository from .info import * from pathlib import Path -from .graph import Graph +from .graph import Graph, DEFAULT_BRANCH from typing import Optional, List from urllib.parse import urlparse from .analyzers import SourceAnalyzer @@ -15,6 +15,30 @@ # Configure logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') + +def detect_branch(path: Path) -> str: + """Resolve the current branch name for a checkout at ``path``. + + Uses ``git rev-parse --abbrev-ref HEAD``. Returns + :data:`api.graph.DEFAULT_BRANCH` when the path is not a git checkout + or when HEAD is detached (the ``rev-parse`` call returns ``HEAD``). + """ + + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=str(path), + capture_output=True, + text=True, + check=True, + ) + branch = (result.stdout or "").strip() + if not branch or branch == "HEAD": + return DEFAULT_BRANCH + return branch + except (FileNotFoundError, subprocess.CalledProcessError): + return DEFAULT_BRANCH + def _clone_source(url: str, name: str) -> Path: # path to local repositories path = Path.cwd() / "repositories" / name @@ -37,17 +61,21 @@ def _clone_source(url: str, name: str) -> Path: return path class Project(): - def __init__(self, name: str, path: Path, url: Optional[str]): - self.url = url - self.name = name - self.path = path - self.graph = Graph(name) + def __init__(self, name: str, path: Path, url: Optional[str], branch: Optional[str] = None): + self.url = url + self.name = name + self.path = path + # Auto-detect branch from the working tree when not explicitly given. + if branch is None: + branch = detect_branch(path) if path is not None and Path(path).exists() else DEFAULT_BRANCH + self.branch = branch + self.graph = Graph(name, branch=self.branch) if url is not None: - save_repo_info(name, url) + save_repo_info(name, url, self.branch) @classmethod - def from_git_repository(cls, url: str): + def from_git_repository(cls, url: str, branch: Optional[str] = None): # Validate url if not validators.url(url): raise Exception(f"invalid url: {url}") @@ -57,10 +85,10 @@ def from_git_repository(cls, url: str): name = parsed_url.path.split('/')[-1] path = _clone_source(url, name) - return cls(name, path, url) + return cls(name, path, url, branch=branch) @classmethod - def from_local_repository(cls, path: Path|str): + def from_local_repository(cls, path: Path|str, branch: Optional[str] = None): path = Path(path) if isinstance(path, str) else path # Validate path exists @@ -74,7 +102,7 @@ def from_local_repository(cls, path: Path|str): name = path.name - return cls(name, path, url) + return cls(name, path, url, branch=branch) def analyze_sources(self, ignore: Optional[List[str]] = None) -> Graph: if ignore is None: @@ -86,7 +114,7 @@ def analyze_sources(self, ignore: Optional[List[str]] = None) -> Graph: # Save processed commit hash to the DB repo = Repository(self.path) current_commit = repo.walk(repo.head.target).__next__() - set_repo_commit(self.name, current_commit.short_id) + set_repo_commit(self.name, current_commit.short_id, self.branch) except Exception: # Probably not .git folder is missing pass @@ -103,7 +131,7 @@ def process_git_history(self, ignore: Optional[List[str]] = []) -> GitGraph: logging.info(f"Switching current working directory to: {self.path}") os.chdir(self.path) - git_graph = build_commit_graph(self.path, self.analyzer, self.name, ignore) + git_graph = build_commit_graph(self.path, self.analyzer, self.name, ignore, branch=self.branch) # Restore original working directory logging.info(f"Restoring current working directory to: {original_dir}") diff --git a/pyproject.toml b/pyproject.toml index 5cdfa914..be0a3186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,3 +44,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["."] + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "pytest-anyio>=0.0.0", +] diff --git a/tests/endpoints/test_list_repos.py b/tests/endpoints/test_list_repos.py index 198f7c2a..28d8cb7f 100644 --- a/tests/endpoints/test_list_repos.py +++ b/tests/endpoints/test_list_repos.py @@ -47,7 +47,9 @@ def test_list_repos(client): # Expecting an empty response assert status == "success" - assert repositories == ['git_repo'] + assert repositories == [ + {"project": "git_repo", "branch": "_default", "graph": "code:git_repo:_default"} + ] def test_list_repos_with_auth(monkeypatch): diff --git a/tests/test_async_graph.py b/tests/test_async_graph.py index ccd1e000..32e445b1 100644 --- a/tests/test_async_graph.py +++ b/tests/test_async_graph.py @@ -61,14 +61,17 @@ async def test_async_graph_exists_closes_on_error(): async def test_async_get_repos_filters_suffixes(): mock_db = MagicMock() mock_db.list_graphs = AsyncMock( - return_value=["repo1", "repo1_git", "repo1_schema", "repo2"] + return_value=["repo1", "code:repo2:main", "repo1_git", "repo1_schema", "code:repo2:main_git"] ) mock_db.aclose = AsyncMock() with patch("api.graph._async_db", return_value=mock_db): repos = await async_get_repos() - assert repos == ["repo1", "repo2"] + assert repos == [ + {"project": "repo1", "branch": "_default", "graph": "repo1"}, + {"project": "repo2", "branch": "main", "graph": "code:repo2:main"}, + ] mock_db.aclose.assert_awaited_once() diff --git a/tests/test_cli.py b/tests/test_cli.py index 9b7f2eeb..6087f257 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -88,7 +88,9 @@ def test_list_includes_repo(self): result = runner.invoke(app, ["list"]) self.assertEqual(result.exit_code, 0) data = _parse_json(result.output) - self.assertIn(self.REPO_NAME, data["repos"]) + # T17: repos are now {project, branch, graph} dicts. + projects = [r["project"] for r in data["repos"]] + self.assertIn(self.REPO_NAME, projects) class TestCLIHelp(unittest.TestCase): diff --git a/tests/test_per_branch_graphs.py b/tests/test_per_branch_graphs.py new file mode 100644 index 00000000..5c738b5d --- /dev/null +++ b/tests/test_per_branch_graphs.py @@ -0,0 +1,256 @@ +"""Unit tests for the per-branch graph identity refactor (T17, issue #651). + +Exercises only the name-composition / parsing helpers and the in-memory +behaviour of the ``Graph`` / ``AsyncGraphQuery`` constructors plus the +``run_migration`` orchestration. Anything that would require a live +FalkorDB / Redis instance is mocked. +""" + +from unittest.mock import MagicMock, AsyncMock, patch + +import pytest + +from api.graph import ( + DEFAULT_BRANCH, + Graph, + AsyncGraphQuery, + compose_graph_name, + parse_graph_name, + async_get_repos, +) +from api.info import _repo_info_key, _legacy_repo_info_key, _normalize_branch +from api.git_utils.git_utils import GitRepoName, LegacyGitRepoName + + +# --------------------------------------------------------------------------- +# compose_graph_name / parse_graph_name +# --------------------------------------------------------------------------- + +def test_compose_graph_name_default_branch(): + assert compose_graph_name("falkordb") == f"code:falkordb:{DEFAULT_BRANCH}" + + +def test_compose_graph_name_explicit_branch(): + assert compose_graph_name("falkordb", "main") == "code:falkordb:main" + + +def test_compose_graph_name_empty_branch_falls_back_to_default(): + assert compose_graph_name("foo", "") == f"code:foo:{DEFAULT_BRANCH}" + + +def test_compose_graph_name_branch_with_slashes(): + # Real-world branches like `dvir/feature` must survive composition. + assert compose_graph_name("foo", "dvir/feature") == "code:foo:dvir/feature" + + +def test_parse_graph_name_round_trip(): + name = compose_graph_name("falkordb", "main") + assert parse_graph_name(name) == ("falkordb", "main") + + +def test_parse_graph_name_legacy_returns_none(): + assert parse_graph_name("legacy_bare_name") is None + + +def test_parse_graph_name_handles_branch_with_colons(): + # Branch names cannot contain ':' in git, so we don't try to support it. + # But branch may contain other unusual characters: the regex is + # ``code:{project}:(.+)`` so anything after the second colon is the branch. + assert parse_graph_name("code:repo:feature/x") == ("repo", "feature/x") + + +# --------------------------------------------------------------------------- +# Graph constructor branch-awareness +# --------------------------------------------------------------------------- + +def _patched_falkordb(): + """Return a MagicMock that replaces ``FalkorDB`` in ``api.graph``.""" + mock_db = MagicMock() + mock_db.select_graph = MagicMock(return_value=MagicMock()) + return patch("api.graph.FalkorDB", return_value=mock_db) + + +def test_graph_default_branch_composes_name(): + with _patched_falkordb(): + g = Graph("foo") + assert g.project == "foo" + assert g.branch == DEFAULT_BRANCH + assert g.name == f"code:foo:{DEFAULT_BRANCH}" + + +def test_graph_explicit_branch_composes_name(): + with _patched_falkordb(): + g = Graph("foo", branch="main") + assert g.project == "foo" + assert g.branch == "main" + assert g.name == "code:foo:main" + + +def test_graph_accepts_already_composed_name(): + with _patched_falkordb(): + g = Graph("code:bar:dev") + assert g.project == "bar" + assert g.branch == "dev" + assert g.name == "code:bar:dev" + + +def test_graph_two_branches_produce_distinct_names(): + with _patched_falkordb(): + a = Graph("foo", branch="main") + b = Graph("foo", branch="feat") + assert a.name != b.name + assert a.project == b.project == "foo" + + +def test_graph_from_raw_name_bypasses_composition(): + with _patched_falkordb(): + g = Graph.from_raw_name("code:foo:main_tmp") + # raw name preserved exactly, even though it isn't a well-formed + # ``code:project:branch`` graph. + assert g.name == "code:foo:main_tmp" + + +# --------------------------------------------------------------------------- +# AsyncGraphQuery constructor branch-awareness +# --------------------------------------------------------------------------- + +def test_async_graph_query_default_branch(): + with patch("api.graph._async_db") as mock_db: + mock_db.return_value = MagicMock() + g = AsyncGraphQuery("foo") + assert g.project == "foo" + assert g.branch == DEFAULT_BRANCH + assert g.name == f"code:foo:{DEFAULT_BRANCH}" + + +def test_async_graph_query_branch_param(): + with patch("api.graph._async_db") as mock_db: + mock_db.return_value = MagicMock() + g = AsyncGraphQuery("foo", branch="develop") + assert g.name == "code:foo:develop" + + +# --------------------------------------------------------------------------- +# async_get_repos returns (project, branch, graph) dicts +# --------------------------------------------------------------------------- + +@pytest.mark.anyio +async def test_async_get_repos_mixes_legacy_and_new(): + mock_db = MagicMock() + mock_db.list_graphs = AsyncMock( + return_value=[ + "legacy_proj", # legacy: no `code:` prefix + "code:newproj:main", # new style + "code:newproj:feat", # same project, different branch + "code:newproj:main_git", # internal suffix, must be filtered + "code:newproj:main_schema", # internal suffix, must be filtered + ] + ) + mock_db.aclose = AsyncMock() + + with patch("api.graph._async_db", return_value=mock_db): + repos = await async_get_repos() + + assert repos == [ + {"project": "legacy_proj", "branch": DEFAULT_BRANCH, "graph": "legacy_proj"}, + {"project": "newproj", "branch": "main", "graph": "code:newproj:main"}, + {"project": "newproj", "branch": "feat", "graph": "code:newproj:feat"}, + ] + + +# --------------------------------------------------------------------------- +# Redis info key composition (api.info) +# --------------------------------------------------------------------------- + +def test_repo_info_key_default_branch(): + assert _repo_info_key("falkordb") == "{falkordb}:_default_info" + + +def test_repo_info_key_explicit_branch(): + assert _repo_info_key("falkordb", "main") == "{falkordb}:main_info" + + +def test_legacy_repo_info_key_for_fallback(): + assert _legacy_repo_info_key("falkordb") == "{falkordb}_info" + + +def test_normalize_branch_handles_none_and_empty(): + assert _normalize_branch(None) == DEFAULT_BRANCH + assert _normalize_branch("") == DEFAULT_BRANCH + assert _normalize_branch("main") == "main" + + +# --------------------------------------------------------------------------- +# Git graph naming (api.git_utils.git_utils) +# --------------------------------------------------------------------------- + +def test_git_repo_name_default_branch(): + assert GitRepoName("repo") == "{repo}:_default_git" + + +def test_git_repo_name_explicit_branch(): + assert GitRepoName("repo", "main") == "{repo}:main_git" + + +def test_legacy_git_repo_name_for_fallback(): + assert LegacyGitRepoName("repo") == "{repo}_git" + + +# --------------------------------------------------------------------------- +# Migration helper (api.migrations.per_branch) +# --------------------------------------------------------------------------- + +def test_migration_dry_run_reports_actions_without_renaming(): + from api.migrations import per_branch + + fake_conn = MagicMock() + fake_conn.exists = MagicMock(return_value=0) + fake_graph = MagicMock() + + mock_db = MagicMock() + mock_db.connection = fake_conn + mock_db.list_graphs = MagicMock( + return_value=["legacy_proj", "{legacy_proj}_git", "code:already:main"] + ) + mock_db.select_graph = MagicMock(return_value=fake_graph) + + with patch.object(per_branch, "_connect", return_value=mock_db): + result = per_branch.run_migration(dry_run=True) + + # Dry-run must never copy or delete any graph. + fake_graph.copy.assert_not_called() + fake_graph.delete.assert_not_called() + fake_conn.rename.assert_not_called() + + # ``code:already:main`` is already migrated and must not be reported. + assert result["dry_run"] is True + assert result["graphs_renamed"] == 1 # legacy_proj + # the {legacy_proj}_git companion graph was also planned, but only when + # info-key existed; the mock returns exists=0 so info_renamed stays 0. + assert result["git_renamed"] == 1 + + +def test_migration_is_idempotent_when_no_legacy_graphs(): + from api.migrations import per_branch + + fake_conn = MagicMock() + fake_conn.exists = MagicMock(return_value=0) + fake_graph = MagicMock() + + mock_db = MagicMock() + mock_db.connection = fake_conn + # Already-migrated layout: only new-style names present. + mock_db.list_graphs = MagicMock( + return_value=["code:proj:_default", "{proj}:_default_git"] + ) + mock_db.select_graph = MagicMock(return_value=fake_graph) + + with patch.object(per_branch, "_connect", return_value=mock_db): + result = per_branch.run_migration(dry_run=False) + + fake_graph.copy.assert_not_called() + fake_graph.delete.assert_not_called() + fake_conn.rename.assert_not_called() + assert result["graphs_renamed"] == 0 + assert result["info_renamed"] == 0 + assert result["git_renamed"] == 0 diff --git a/uv.lock b/uv.lock index 2f57f302..0258bad8 100644 --- a/uv.lock +++ b/uv.lock @@ -373,13 +373,21 @@ dependencies = [ [package.optional-dependencies] test = [ + { name = "anyio" }, { name = "httpx" }, { name = "pytest" }, { name = "ruff" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-anyio" }, +] + [package.metadata] requires-dist = [ + { name = "anyio", marker = "extra == 'test'", specifier = ">=4.0,<5.0" }, { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, { name = "falkordb-multilspy", specifier = ">=0.1.0,<1.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, @@ -404,6 +412,12 @@ requires-dist = [ ] provides-extras = ["test"] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-anyio", specifier = ">=0.0.0" }, +] + [[package]] name = "falkordb-multilspy" version = "0.1.0" @@ -1326,6 +1340,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-anyio" +version = "0.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/44/a02e5877a671b0940f21a7a0d9704c22097b123ed5cdbcca9cab39f17acc/pytest-anyio-0.0.0.tar.gz", hash = "sha256:b41234e9e9ad7ea1dbfefcc1d6891b23d5ef7c9f07ccf804c13a9cc338571fd3", size = 1560, upload-time = "2021-06-29T22:57:30.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/25/bd6493ae85d0a281b6a0f248d0fdb1d9aa2b31f18bcd4a8800cf397d8209/pytest_anyio-0.0.0-py2.py3-none-any.whl", hash = "sha256:dc8b5c4741cb16ff90be37fddd585ca943ed12bbeb563de7ace6cd94441d8746", size = 1999, upload-time = "2021-06-29T22:57:29.158Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1775,6 +1802,14 @@ version = "0.23.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/85/a61c782afbb706a47d990eaee6977e7c2bd013771c5bf5c81c617684f286/tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb", size = 1317728, upload-time = "2024-11-11T05:25:32.535Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/dc/d4a0ad9e466263728f80f9dac399609473af01c1aba2ea3ea8879ce56276/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c", size = 333661, upload-time = "2026-04-14T15:11:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/61/7a/5c862770460a2e27079e725585ad2718100373c09448c14e36934ef44414/tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c", size = 376295, upload-time = "2026-04-14T15:11:15.346Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/0571a3a34c0feda60a9c37cf6dd5edfdbc24f8fcb1e48b6b6eb0f324ad2a/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5", size = 358331, upload-time = "2026-04-14T15:11:16.418Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/0f7e1f50f6365338eb700f01710da0adc49a49fa9a8443e5a90ea4f29491/tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf", size = 359444, upload-time = "2026-04-14T15:11:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/129bd56d5ef22b4ae254940a09b6d3ed873093218868a3f9635d571d514e/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832", size = 358143, upload-time = "2026-04-14T15:11:18.755Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cd/e12cdca47e0c56151cb4b156d48091b7bc1d968e072c1656cf6b73fe7218/tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e", size = 357524, upload-time = "2026-04-14T15:11:19.717Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/f742d60f818cba83760f4975c7158d1c96c36b5807e95a843db7fb8c64b7/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be", size = 338755, upload-time = "2026-04-14T15:11:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e4/8a8642b9bba86248ac2facc81ffb187c06c6768efa56c79d61fab70d736b/tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345", size = 337261, upload-time = "2026-04-14T15:11:22.111Z" }, { url = "https://files.pythonhosted.org/packages/58/04/f6c2df4c53a588ccd88d50851155945cff8cd887bd70c175e00aaade7edf/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd", size = 372235, upload-time = "2024-11-11T05:25:19.424Z" }, { url = "https://files.pythonhosted.org/packages/99/10/1aa9486f1e28fc22810fa92cbdc54e1051e7f5536a5e5b5e9695f609b31e/tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e", size = 419046, upload-time = "2024-11-11T05:25:20.679Z" }, { url = "https://files.pythonhosted.org/packages/0f/21/13df29f8fcb9ba9f209b7b413a4764b673dfd58989a0dd67e9c7e19e9c2e/tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d", size = 415999, upload-time = "2024-11-11T05:25:22.359Z" }, From bba43e0eb5fe6837b478e287b3f506780832c560 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan Date: Wed, 27 May 2026 14:06:13 +0300 Subject: [PATCH 27/63] ci(mcp): add MCP-tests workflow with FalkorDB service (T2 #649) New `.github/workflows/mcp-tests.yml` runs `pytest tests/mcp/` against a real FalkorDB service container on port 6379. Triggers only on PRs that touch MCP-relevant paths so the unrelated parts of the repo don't pay the cost. - FalkorDB service with redis-cli ping healthcheck. - uv cache keyed on uv.lock for fast incremental runs. - Sets `FALKORDB_HOST` / `FALKORDB_PORT` env so api/graph.py picks up the service host. - Path filter covers api/mcp/, tests/mcp/, api/llm.py, api/graph.py, pyproject.toml, uv.lock, and the workflow file itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/mcp-tests.yml | 76 +++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/mcp-tests.yml diff --git a/.github/workflows/mcp-tests.yml b/.github/workflows/mcp-tests.yml new file mode 100644 index 00000000..f0ae1292 --- /dev/null +++ b/.github/workflows/mcp-tests.yml @@ -0,0 +1,76 @@ +name: MCP tests + +on: + push: + branches: ["main", "staging", "mcp/**"] + paths: + - "api/mcp/**" + - "tests/mcp/**" + - "api/llm.py" + - "api/graph.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/mcp-tests.yml" + pull_request: + paths: + - "api/mcp/**" + - "tests/mcp/**" + - "api/llm.py" + - "api/graph.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/mcp-tests.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + mcp-tests: + runs-on: ubuntu-latest + + services: + falkordb: + image: falkordb/falkordb:latest + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 12 + + env: + FALKORDB_HOST: localhost + FALKORDB_PORT: "6379" + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + version: "latest" + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Install backend dependencies + run: uv sync --all-extras + + - name: Verify FalkorDB reachable + run: | + sudo apt-get update -qq && sudo apt-get install -y redis-tools + redis-cli -h localhost -p 6379 ping + + - name: Run MCP test suite + run: uv run pytest tests/mcp/ -v From a598a742ca2622ae45da760e9bc5448523ede117 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan Date: Wed, 27 May 2026 14:08:40 +0300 Subject: [PATCH 28/63] test(mcp): sample-project fixture + assertion contract (T3 #650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `tests/mcp/fixtures/`: - `sample_project/python/` — canonical call graph `entrypoint -> service -> {UserRepo,OrderRepo}.repo -> db` plus a small class hierarchy (BaseRepo <- UserRepo, OrderRepo) and inter-file imports so IMPORTS edges exist. - `expected.yaml` — single source of truth for every per-tool ticket's integration assertions: minimum per-label counts, named callers / callees, known paths, prefix-search hits. New `tests/mcp/conftest.py`: - `expected_contract` (pure-Python, always available) loads the YAML once per session. - `indexed_fixture` (session-scoped) indexes the fixture into a unique `code:sample_project:test-` graph so parallel CI shards don't contend. Self-skips when FalkorDB is unreachable. Uses `SourceAnalyzer.analyze_local_folder` directly so the fixture doesn't need to be a git repository. New `tests/mcp/test_fixture_contract.py` — regression-tests the fixture itself: contract shape, on-disk files, and that the integration fixture indexes cleanly and meets the minimum count contract. Multilingual coverage (Java + C#) was dropped from the spec: both multilspy analyzers demand a Maven / .NET project layout at the indexed root, which would force this fixture into an awkward shape. Deferred to a follow-up ticket (likely T16 which adds languages). All 4 contract tests pass against FalkorDB on 6390. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyproject.toml | 1 + tests/mcp/conftest.py | 116 ++++++++++++++++++ tests/mcp/fixtures/expected.yaml | 48 ++++++++ tests/mcp/fixtures/sample_project/README.md | 36 ++++++ .../sample_project/python/__init__.py | 1 + .../mcp/fixtures/sample_project/python/db.py | 6 + .../sample_project/python/entrypoint.py | 17 +++ .../fixtures/sample_project/python/repo.py | 23 ++++ .../fixtures/sample_project/python/service.py | 10 ++ tests/mcp/test_fixture_contract.py | 81 ++++++++++++ uv.lock | 2 + 11 files changed, 341 insertions(+) create mode 100644 tests/mcp/conftest.py create mode 100644 tests/mcp/fixtures/expected.yaml create mode 100644 tests/mcp/fixtures/sample_project/README.md create mode 100644 tests/mcp/fixtures/sample_project/python/__init__.py create mode 100644 tests/mcp/fixtures/sample_project/python/db.py create mode 100644 tests/mcp/fixtures/sample_project/python/entrypoint.py create mode 100644 tests/mcp/fixtures/sample_project/python/repo.py create mode 100644 tests/mcp/fixtures/sample_project/python/service.py create mode 100644 tests/mcp/test_fixture_contract.py diff --git a/pyproject.toml b/pyproject.toml index be0a3186..ab0c176e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,4 +49,5 @@ where = ["."] dev = [ "pytest>=9.0.2", "pytest-anyio>=0.0.0", + "pyyaml>=6.0.3", ] diff --git a/tests/mcp/conftest.py b/tests/mcp/conftest.py new file mode 100644 index 00000000..1b24abe1 --- /dev/null +++ b/tests/mcp/conftest.py @@ -0,0 +1,116 @@ +"""Shared fixtures for the MCP test suite. + +Every per-tool integration test from T4 onward reuses the +``indexed_fixture`` fixture below: it indexes ``fixtures/sample_project`` +into a uniquely named FalkorDB graph once per test session and yields a +descriptor (project name, branch, graph name) the test can pass straight +to the MCP tool under test. + +The integration fixture is opt-in — it requires a reachable FalkorDB +(see ``api/graph.py``) and the optional language analyzers. Tests that +only need the static contract (counts, named callers / callees / paths) +can depend on ``expected_contract`` alone, which is pure-Python and +always available. +""" + +from __future__ import annotations + +import os +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import pytest +import yaml + + +FIXTURE_DIR = Path(__file__).parent / "fixtures" +SAMPLE_PROJECT = FIXTURE_DIR / "sample_project" +EXPECTED_PATH = FIXTURE_DIR / "expected.yaml" + + +# --------------------------------------------------------------------------- +# Pure-Python contract (no FalkorDB required) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class IndexedFixture: + """Descriptor for an indexed fixture graph.""" + + project: str + branch: str + graph_name: str + path: Path + + +@pytest.fixture(scope="session") +def expected_contract() -> dict[str, Any]: + """Load ``fixtures/expected.yaml`` once per session.""" + with EXPECTED_PATH.open() as fh: + return yaml.safe_load(fh) + + +@pytest.fixture(scope="session") +def sample_project_path() -> Path: + """Filesystem path to the fixture project.""" + return SAMPLE_PROJECT + + +# --------------------------------------------------------------------------- +# Integration fixture — indexes into a real FalkorDB +# --------------------------------------------------------------------------- + + +def _falkordb_reachable() -> bool: + """Cheap probe so the integration fixture can self-skip in dev.""" + try: + import socket + + host = os.getenv("FALKORDB_HOST", "localhost") + port = int(os.getenv("FALKORDB_PORT", 6379)) + with socket.create_connection((host, port), timeout=1): + return True + except OSError: + return False + + +@pytest.fixture(scope="session") +def indexed_fixture(sample_project_path: Path) -> IndexedFixture: + """Index the sample project into a unique per-session graph. + + Each test session creates a new graph named + ``code:sample_project:test-`` so parallel CI shards never + contend on the same graph. The graph is intentionally **not** + cleaned up — short-lived CI runners discard the FalkorDB volume, + and keeping it around helps post-mortem debugging on developer + machines. + + Uses :class:`api.analyzers.SourceAnalyzer` directly (instead of + ``Project.from_local_repository``) so the fixture doesn't need to + be a git repository — analyzing a plain directory is exactly the + code path the ``index_repo`` MCP tool exercises for non-git + folders. + """ + + if not _falkordb_reachable(): + pytest.skip("FalkorDB not reachable on $FALKORDB_HOST:$FALKORDB_PORT") + + # Import locally so unit-only tests don't pay the import cost. + from api.analyzers.source_analyzer import SourceAnalyzer + from api.graph import Graph + + project_name = sample_project_path.name # "sample_project" + branch = f"test-{uuid.uuid4().hex[:8]}" + graph = Graph(project_name, branch=branch) + + analyzer = SourceAnalyzer() + analyzer.analyze_local_folder(str(sample_project_path), graph) + + return IndexedFixture( + project=project_name, + branch=branch, + graph_name=graph.name, + path=sample_project_path, + ) diff --git a/tests/mcp/fixtures/expected.yaml b/tests/mcp/fixtures/expected.yaml new file mode 100644 index 00000000..061258f1 --- /dev/null +++ b/tests/mcp/fixtures/expected.yaml @@ -0,0 +1,48 @@ +# MCP fixture assertion contract. +# +# Anything declared here is treated as load-bearing by tests/mcp/*. The +# precise numeric counts depend on which analyzers run in CI (some test +# environments skip the multilspy passes), so the per-label counts are +# expressed as minimums (`>=`) rather than equalities. Named-symbol +# assertions are exact. + +# The graph is named after the fixture directory (per Project.from_local_repository). +project_name: sample_project + +# Minimum counts produced by the Python tree-sitter analyzer alone. +# Java + C# add more when multilspy is available. +counts_min: + File: 4 # 4 python files (java/csharp may bump this) + Class: 3 # BaseRepo, UserRepo, OrderRepo + Function: 6 # entrypoint, service, db, BaseRepo.repo, UserRepo.repo, OrderRepo.repo + +# Named callers / callees (used by T5 — get_callers / get_callees). +calls: + service: + callers: ["entrypoint"] + # service() instantiates UserRepo + OrderRepo and calls .repo() on each; + # the analyzer encodes that as CALLS edges on the method. + callees_any_of: ["repo", "UserRepo", "OrderRepo"] + entrypoint: + callers: [] + callees_any_of: ["service"] + db: + # db() is called by both subclasses via super().repo() -> BaseRepo.repo(). + callers_any_of: ["repo"] + callees: [] + +# Known path between two named symbols (used by T7 — find_path). +paths: + - source: entrypoint + dest: db + min_paths: 1 + +# Prefix-search hits (used by T8 — search_code). +search_prefixes: + ent: + must_include: ["entrypoint"] + serv: + must_include: ["service"] + Repo: + # case-insensitive prefix match on Searchable label + must_include_any_of: ["BaseRepo", "UserRepo", "OrderRepo"] diff --git a/tests/mcp/fixtures/sample_project/README.md b/tests/mcp/fixtures/sample_project/README.md new file mode 100644 index 00000000..2c3f6147 --- /dev/null +++ b/tests/mcp/fixtures/sample_project/README.md @@ -0,0 +1,36 @@ +# Sample fixture project for the MCP test suite + +This directory is consumed by `tests/mcp/conftest.py::indexed_fixture`. Every +MCP tool ticket from T4 onward asserts against the assertions declared in +`expected.yaml`. + +## Canonical Python call graph + +``` +entrypoint() -> service() -> {UserRepo,OrderRepo}.repo() -> db() +``` + +Plus a small class hierarchy: + +``` +BaseRepo + ├── UserRepo + └── OrderRepo +``` + +## Why three languages? (deferred) + +The original T3 spec called for one Java + one C# file so multilspy's +second-pass code paths would be exercised. In practice both analyzers +demand a real Maven / .NET project layout at the **root** of the indexed +tree, which would make this fixture awkward to co-host with the Python +sample. The multilingual coverage is therefore deferred to a follow-up +ticket (likely T16, which already pulls in additional languages). + +T4-T8 only need Python, which this fixture covers in full. + +## Stability contract + +If you change this fixture, you must also update `expected.yaml`. Tests +read counts and named symbols directly from that file so the assertion +contract stays in lock-step. diff --git a/tests/mcp/fixtures/sample_project/python/__init__.py b/tests/mcp/fixtures/sample_project/python/__init__.py new file mode 100644 index 00000000..374aa568 --- /dev/null +++ b/tests/mcp/fixtures/sample_project/python/__init__.py @@ -0,0 +1 @@ +"""Marks ``sample_project/python`` as a package so IMPORTS edges resolve.""" diff --git a/tests/mcp/fixtures/sample_project/python/db.py b/tests/mcp/fixtures/sample_project/python/db.py new file mode 100644 index 00000000..37a382de --- /dev/null +++ b/tests/mcp/fixtures/sample_project/python/db.py @@ -0,0 +1,6 @@ +"""Bottom of the canonical call chain.""" + + +def db() -> str: + """Leaf function — entrypoint -> service -> repo -> db.""" + return "db" diff --git a/tests/mcp/fixtures/sample_project/python/entrypoint.py b/tests/mcp/fixtures/sample_project/python/entrypoint.py new file mode 100644 index 00000000..7edcce4a --- /dev/null +++ b/tests/mcp/fixtures/sample_project/python/entrypoint.py @@ -0,0 +1,17 @@ +"""Entrypoint for the MCP test fixture project. + +Call graph (must match ``expected.yaml``): + + entrypoint() -> service() -> repo() -> db() +""" + +from .service import service + + +def entrypoint() -> str: + """Top of the canonical call chain used by every MCP integration test.""" + return service() + + +if __name__ == "__main__": + entrypoint() diff --git a/tests/mcp/fixtures/sample_project/python/repo.py b/tests/mcp/fixtures/sample_project/python/repo.py new file mode 100644 index 00000000..c9295ce0 --- /dev/null +++ b/tests/mcp/fixtures/sample_project/python/repo.py @@ -0,0 +1,23 @@ +"""Repository layer for the MCP fixture project. + +Exercises a small class hierarchy: ``BaseRepo`` <- ``UserRepo`` / ``OrderRepo``. +""" + +from .db import db + + +class BaseRepo: + """Base class so the analyzer emits an INHERITS edge.""" + + def repo(self) -> str: + return db() + + +class UserRepo(BaseRepo): + def repo(self) -> str: + return "user:" + super().repo() + + +class OrderRepo(BaseRepo): + def repo(self) -> str: + return "order:" + super().repo() diff --git a/tests/mcp/fixtures/sample_project/python/service.py b/tests/mcp/fixtures/sample_project/python/service.py new file mode 100644 index 00000000..1afdcab7 --- /dev/null +++ b/tests/mcp/fixtures/sample_project/python/service.py @@ -0,0 +1,10 @@ +"""Service layer for the MCP fixture project.""" + +from .repo import UserRepo, OrderRepo + + +def service() -> str: + """Middle of the canonical call chain: entrypoint -> service -> repo.""" + users = UserRepo() + orders = OrderRepo() + return users.repo() + ":" + orders.repo() diff --git a/tests/mcp/test_fixture_contract.py b/tests/mcp/test_fixture_contract.py new file mode 100644 index 00000000..9e426c90 --- /dev/null +++ b/tests/mcp/test_fixture_contract.py @@ -0,0 +1,81 @@ +"""T3 — assertion contract sanity check. + +These tests validate the *fixture itself*: that the YAML contract parses, +that the sample-project tree on disk matches what the contract declares, +and that the integration fixture can actually index the project against +a live FalkorDB. Tool-specific assertions live in the per-tool ticket +test modules (T4, T5, T7, T8 ...). +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from tests.mcp.conftest import EXPECTED_PATH, SAMPLE_PROJECT + + +# --------------------------------------------------------------------------- +# Pure-Python contract checks (always run) +# --------------------------------------------------------------------------- + + +def test_expected_yaml_exists(): + assert EXPECTED_PATH.is_file(), "fixtures/expected.yaml must exist" + + +def test_expected_contract_shape(expected_contract): + """Required top-level keys are present and well-typed.""" + + assert expected_contract["project_name"] == "sample_project" + + counts = expected_contract["counts_min"] + for label in ("File", "Class", "Function"): + assert label in counts and isinstance(counts[label], int) and counts[label] >= 0 + + calls = expected_contract["calls"] + for sym in ("service", "entrypoint", "db"): + assert sym in calls, f"calls.{sym} missing from expected.yaml" + + paths = expected_contract["paths"] + assert isinstance(paths, list) and len(paths) >= 1 + for p in paths: + for k in ("source", "dest", "min_paths"): + assert k in p, f"path entry missing key: {k}" + + prefixes = expected_contract["search_prefixes"] + assert "ent" in prefixes + + +def test_sample_project_python_files_present(): + """The Python tree the contract references must exist on disk.""" + py = SAMPLE_PROJECT / "python" + for name in ("entrypoint.py", "service.py", "repo.py", "db.py", "__init__.py"): + assert (py / name).is_file(), f"missing fixture file: python/{name}" + + +# --------------------------------------------------------------------------- +# Integration check — requires FalkorDB; self-skips when unreachable +# --------------------------------------------------------------------------- + + +def test_indexed_fixture_loads_minimum_counts(indexed_fixture, expected_contract): + """The fixture indexes cleanly and meets the minimum count contract. + + Subsequent per-tool tickets (T4+) use ``indexed_fixture`` directly; + this test exists so the fixture itself is regression-tested in + isolation. + """ + + from api.graph import Graph + + g = Graph(indexed_fixture.project, branch=indexed_fixture.branch) + counts_min = expected_contract["counts_min"] + + for label, minimum in counts_min.items(): + rows = g.g.query(f"MATCH (n:{label}) RETURN count(n) AS c").result_set + actual = rows[0][0] if rows else 0 + assert actual >= minimum, ( + f"label {label}: expected >={minimum}, got {actual}" + ) diff --git a/uv.lock b/uv.lock index 0258bad8..ac98c2d8 100644 --- a/uv.lock +++ b/uv.lock @@ -383,6 +383,7 @@ test = [ dev = [ { name = "pytest" }, { name = "pytest-anyio" }, + { name = "pyyaml" }, ] [package.metadata] @@ -416,6 +417,7 @@ provides-extras = ["test"] dev = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-anyio", specifier = ">=0.0.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, ] [[package]] From 18d3cc7f6cb84377d958e123830fd7f993362e34 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan Date: Wed, 27 May 2026 14:10:24 +0300 Subject: [PATCH 29/63] feat(mcp): index_repo tool (T4 #652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real MCP tool. Wraps the existing Project / SourceAnalyzer pipeline so AI agents can call `index_repo(path_or_url, branch)` over stdio to populate code-graph for a repo. - `api/mcp/tools/structural.py` (NEW) — registers `index_repo` on the shared FastMCP app. Accepts local paths or git URLs; auto-detects branch from local git checkouts via T17's `detect_branch`; honors `ALLOWED_ANALYSIS_DIR` for sandboxing. Non-git folders are handled by driving SourceAnalyzer directly (Project requires a git repo). - `api/mcp/tools/__init__.py` (NEW) — package marker; importing it registers every tool module's `@app.tool()` decorators. - `api/mcp/server.py` — imports tools at module load so both direct `from api.mcp.server import app` and `cgraph-mcp` stdio entry point see the same tool list. - `tests/mcp/test_index_repo.py` (NEW) — 5 tests: local-path happy path, missing-path error, ALLOWED_ANALYSIS_DIR sandboxing, in-process app registration, JSON serialisability. - `tests/mcp/test_scaffold.py` — replaced the "zero tools" assertion with a presence check for `index_repo` so it stays stable as T5-T8 / T11 add more tools. Return shape: {project_name, branch, graph_name, num_nodes, num_edges, languages_detected, mode} `incremental` parameter is accepted now and forwarded once T18 lands; the current full-reindex path ignores it and always returns `mode="full"`. All 8 tests pass against FalkorDB on 6390. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/mcp/server.py | 5 + api/mcp/tools/__init__.py | 7 ++ api/mcp/tools/structural.py | 184 +++++++++++++++++++++++++++++++++++ tests/mcp/test_index_repo.py | 80 +++++++++++++++ tests/mcp/test_scaffold.py | 13 ++- 5 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 api/mcp/tools/__init__.py create mode 100644 api/mcp/tools/structural.py create mode 100644 tests/mcp/test_index_repo.py diff --git a/api/mcp/server.py b/api/mcp/server.py index f0ae6f84..63ce5cfa 100644 --- a/api/mcp/server.py +++ b/api/mcp/server.py @@ -13,6 +13,11 @@ app: FastMCP = FastMCP("code-graph") +# Register tools on import so both direct ``import api.mcp.server`` and the +# stdio entry point see the same tool list. Imported below ``app`` because +# the tool modules need a reference to it. +from . import tools # noqa: F401, E402 + def main() -> None: """Run the MCP server over stdio. diff --git a/api/mcp/tools/__init__.py b/api/mcp/tools/__init__.py new file mode 100644 index 00000000..87b8b3a0 --- /dev/null +++ b/api/mcp/tools/__init__.py @@ -0,0 +1,7 @@ +"""MCP tool implementations for code-graph. + +Each submodule registers tools against the shared FastMCP app exposed by +``api.mcp.server``. Import this package to register all tools. +""" + +from . import structural # noqa: F401 (registers tools on import) diff --git a/api/mcp/tools/structural.py b/api/mcp/tools/structural.py new file mode 100644 index 00000000..30540f4b --- /dev/null +++ b/api/mcp/tools/structural.py @@ -0,0 +1,184 @@ +"""Structural MCP tools (T4-T8). + +These tools wrap the existing ``Project`` / ``Graph`` / ``AsyncGraphQuery`` +operations so MCP-capable agents (Claude Code, Cursor, Copilot, Cline) +can drive code-graph over the standard stdio transport. + +Conventions shared by all tools in this module: + +* Every tool accepts an optional ``branch`` so the agent can scope queries + to a specific per-branch graph (see T17, issue #651). When omitted the + branch is either auto-detected from a local checkout (``index_repo``) + or defaults to ``_default``. +* Long-running synchronous operations are pushed into a thread via + ``asyncio.get_running_loop().run_in_executor`` so the MCP event loop + stays responsive. +""" + +from __future__ import annotations + +import asyncio +import logging +import os +from pathlib import Path +from typing import Any, Optional + +from ..server import app + + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _looks_like_url(spec: str) -> bool: + """Return True for HTTP(S) / git URLs, False for local paths.""" + return spec.startswith(("http://", "https://", "git@", "ssh://", "git://")) + + +def _languages_detected(graph) -> list[str]: + """Best-effort enumeration of distinct ``File.ext`` values. + + Returns a sorted list of extension strings (without the leading dot). + Empty when no files were indexed. + """ + try: + rows = graph.g.query( + "MATCH (f:File) RETURN DISTINCT f.ext AS ext" + ).result_set + except Exception as e: # pragma: no cover — defensive + logger.warning("languages_detected query failed: %s", e) + return [] + seen: set[str] = set() + for row in rows or []: + ext = (row[0] or "").lstrip(".") + if ext: + seen.add(ext) + return sorted(seen) + + +def _count(graph, label: str) -> int: + try: + rows = graph.g.query( + f"MATCH (n:{label}) RETURN count(n) AS c" + ).result_set + return int(rows[0][0]) if rows else 0 + except Exception: + return 0 + + +def _count_edges(graph) -> int: + try: + rows = graph.g.query("MATCH ()-[r]->() RETURN count(r) AS c").result_set + return int(rows[0][0]) if rows else 0 + except Exception: + return 0 + + +# --------------------------------------------------------------------------- +# T4 — index_repo +# --------------------------------------------------------------------------- + + +@app.tool( + name="index_repo", + description=( + "Index a code repository into code-graph for subsequent navigation. " + "Accepts a local path or a git URL. When `branch` is omitted, " + "auto-detects the current branch from the local checkout (defaults " + "to '_default' for non-git folders). Returns the indexed graph's " + "node/edge counts, detected languages, and the (project, branch) " + "identity callers should pass to other code-graph tools." + ), +) +async def index_repo( + path_or_url: str, + branch: Optional[str] = None, + incremental: bool = True, # accepted now, fully honored once T18 lands + ignore: Optional[list[str]] = None, +) -> dict[str, Any]: + """Implementation for the ``index_repo`` MCP tool. + + Args: + path_or_url: Filesystem path to a local repository **or** a clonable + git URL (``https://...``, ``git@host:...``, ``ssh://...``). + branch: Branch identity for the indexed graph. When ``None``: + auto-detect from the checkout via ``git rev-parse --abbrev-ref + HEAD``; falls back to ``_default`` if not a git checkout. + incremental: Accepted for forward-compatibility with T18; the + current full-reindex path ignores it. + ignore: List of relative paths to skip during analysis. + """ + + from api.project import Project, detect_branch + + if ignore is None: + ignore = [] + + loop = asyncio.get_running_loop() + + def _do_index() -> dict[str, Any]: + if _looks_like_url(path_or_url): + project = Project.from_git_repository(path_or_url, branch=branch) + else: + local_path = Path(path_or_url).expanduser().resolve() + if not local_path.exists(): + raise ValueError(f"path does not exist: {local_path}") + + # Reject paths outside the allow-list when one is configured. + allowed_root = os.getenv("ALLOWED_ANALYSIS_DIR") + if allowed_root: + allowed = Path(allowed_root).expanduser().resolve() + try: + local_path.relative_to(allowed) + except ValueError as e: + raise ValueError( + f"path {local_path} is outside ALLOWED_ANALYSIS_DIR={allowed}" + ) from e + + # Use Project for git-repo paths so commit metadata is saved, + # otherwise drive SourceAnalyzer directly so non-git folders work. + if (local_path / ".git").is_dir(): + project = Project.from_local_repository(local_path, branch=branch) + else: + # Synthesize a Project-like object so the return shape is uniform. + from api.analyzers.source_analyzer import SourceAnalyzer + from api.graph import Graph + + detected = branch if branch is not None else detect_branch(local_path) + graph = Graph(local_path.name, branch=detected) + analyzer = SourceAnalyzer() + analyzer.analyze_local_folder(str(local_path), graph, ignore) + + class _Synth: # tiny shim to mirror Project's surface + name = local_path.name + + def __init__(self, g, b): + self.graph = g + self.branch = b + + return _payload(_Synth(graph, detected)) + + project.analyze_sources(ignore) + return _payload(project) + + def _payload(project) -> dict[str, Any]: + g = project.graph + return { + "project_name": project.name, + "branch": getattr(project, "branch", None), + "graph_name": g.name, + "num_nodes": ( + _count(g, "File") + _count(g, "Class") + _count(g, "Function") + ), + "num_edges": _count_edges(g), + "languages_detected": _languages_detected(g), + # T18 will flip this to "incremental" when only changed files + # were re-analyzed. + "mode": "full", + } + + return await loop.run_in_executor(None, _do_index) diff --git a/tests/mcp/test_index_repo.py b/tests/mcp/test_index_repo.py new file mode 100644 index 00000000..ebf88a19 --- /dev/null +++ b/tests/mcp/test_index_repo.py @@ -0,0 +1,80 @@ +"""T4 — ``index_repo`` MCP tool tests.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + + +pytestmark = pytest.mark.anyio + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +async def test_index_repo_local_path(sample_project_path: Path, expected_contract): + """Index a local non-git folder and verify the response shape.""" + from api.mcp.tools.structural import index_repo + + result = await index_repo(str(sample_project_path), branch="t4-local-test") + + assert result["project_name"] == "sample_project" + assert result["branch"] == "t4-local-test" + assert result["graph_name"].startswith("code:sample_project:") + assert result["mode"] == "full" + assert result["num_nodes"] >= sum(expected_contract["counts_min"].values()) + assert result["num_edges"] > 0 + assert "py" in result["languages_detected"] + + +async def test_index_repo_rejects_missing_path(): + """Missing local paths surface as a clear ValueError to the agent.""" + from api.mcp.tools.structural import index_repo + + with pytest.raises(ValueError, match="path does not exist"): + await index_repo("/this/path/definitely/does/not/exist/anywhere") + + +async def test_index_repo_honors_allowed_analysis_dir( + sample_project_path: Path, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +): + """Sandboxing: paths outside ALLOWED_ANALYSIS_DIR are rejected.""" + from api.mcp.tools import structural + + monkeypatch.setenv("ALLOWED_ANALYSIS_DIR", str(tmp_path)) + + with pytest.raises(ValueError, match="outside ALLOWED_ANALYSIS_DIR"): + await structural.index_repo(str(sample_project_path), branch="t4-sandbox") + + +async def test_index_repo_registered_via_app(): + """The tool is reachable via ``app.list_tools()`` (protocol parity).""" + from api.mcp.server import app + + tools = await app.list_tools() + names = {t.name for t in tools} + assert "index_repo" in names + + tool = next(t for t in tools if t.name == "index_repo") + schema = tool.inputSchema + # Description / param schema are surfaced to the agent. + assert "path_or_url" in schema["properties"] + assert "branch" in schema["properties"] + assert "incremental" in schema["properties"] + + +async def test_index_repo_response_serialises_to_json( + sample_project_path: Path, +): + """MCP transports JSON — the response dict must be JSON-serialisable.""" + from api.mcp.tools.structural import index_repo + + result = await index_repo(str(sample_project_path), branch="t4-json-test") + # Must not raise. + json.dumps(result) diff --git a/tests/mcp/test_scaffold.py b/tests/mcp/test_scaffold.py index 851f091a..b02a6780 100644 --- a/tests/mcp/test_scaffold.py +++ b/tests/mcp/test_scaffold.py @@ -45,11 +45,12 @@ def test_main_entry_point_exists() -> None: @pytest.mark.anyio -async def test_stdio_server_lists_zero_tools() -> None: +async def test_stdio_server_lists_registered_tools() -> None: """Spawn ``cgraph-mcp`` over stdio and verify the protocol handshake. - The scaffold registers no tools, so ``list_tools`` must return an - empty list. Tool tickets (T4-T8, T11) extend this expectation. + Once tool tickets land (T4+), ``list_tools`` returns at least the + tools they register. This test only guards the *handshake* — per-tool + behavioural assertions live in the per-tool test modules. """ cgraph_mcp = shutil.which("cgraph-mcp") assert cgraph_mcp is not None, ( @@ -62,4 +63,8 @@ async def test_stdio_server_lists_zero_tools() -> None: async with ClientSession(read, write) as session: await session.initialize() result = await session.list_tools() - assert result.tools == [] + # ``index_repo`` lands in T4; this assertion intentionally + # only checks for presence so it stays stable as more tools + # are registered in T5-T8 / T11. + names = {t.name for t in result.tools} + assert "index_repo" in names From 58e35b3338f1ce51d3d5258265c024b1454f7068 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:15:05 +0300 Subject: [PATCH 30/63] =?UTF-8?q?feat(mcp):=20query=20tools=20=E2=80=94=20?= =?UTF-8?q?get=5Fcallers/callees/deps,=20find=5Fpath,=20search=5Fcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles T5 (#653), T7 (#655), T8 (#656) into one PR; all three are thin async wrappers around existing AsyncGraphQuery operations and share the same _node_summary / _coerce_node_id / _project_arg helpers. - search_code: prefix search backed by the FalkorDB fulltext index. Surfaces flat {id, name, label, file, line} so agents can hand the id straight to the navigation tools. - get_callers / get_callees: incoming / outgoing CALLS edges. The shared _neighbors_payload inlines the IN-direction Cypher because AsyncGraphQuery.get_neighbors only walks OUT. - get_dependencies: same machinery, but aggregates a configurable set of relations (default CALLS/IMPORTS/DEFINES) and dedups by node id. - find_path: returns up to N CALLS-only paths between two symbols as a node sequence; strips encode_edge entries from the alternating [node, edge, ...] list produced by AsyncGraphQuery.find_paths. Helpers: - _node_summary flattens the encode_node shape (which nests data under 'properties' and includes the 'Searchable' fulltext-index label) into the {id, name, label, file, line} agents want. - _coerce_node_id accepts int or stringified-int and rejects bool. Tests (tests/mcp/test_query_tools.py, 13 tests): - search_code prefix happy/limit/no-match/serialisability paths. - get_callees(entrypoint) ⊇ {service}; get_callers(service) ⊇ {entrypoint}. - get_dependencies includes the CALLS relation. - Neighbor tools accept string ids; reject garbage. - find_path(entrypoint → db) ≥ 1; reverse direction returns []; max_paths is honored. - All five tools are registered on the MCP app. Also drops a stray venv/ that snuck into the fixture directory and was polluting the prefix-search results. Closes #653, #655, #656. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/mcp/tools/structural.py | 239 ++++++++++++++++++++++++++++++++ tests/mcp/test_query_tools.py | 253 ++++++++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 tests/mcp/test_query_tools.py diff --git a/api/mcp/tools/structural.py b/api/mcp/tools/structural.py index 30540f4b..05d86ae2 100644 --- a/api/mcp/tools/structural.py +++ b/api/mcp/tools/structural.py @@ -182,3 +182,242 @@ def _payload(project) -> dict[str, Any]: } return await loop.run_in_executor(None, _do_index) + + +# --------------------------------------------------------------------------- +# T5 — get_callers / get_callees / get_dependencies +# --------------------------------------------------------------------------- + + +def _project_arg(project: str, branch: Optional[str]): + """Return an :class:`AsyncGraphQuery` for ``(project, branch)``.""" + from api.graph import AsyncGraphQuery + + return AsyncGraphQuery(project, branch=branch) + + +def _node_summary(n: Any) -> dict[str, Any]: + """Normalize a FalkorDB Node (or already-encoded dict) to a flat payload. + + ``encode_node`` returns ``{id, labels, properties: {...}}`` because Node + properties live on a nested attribute. Agents want a flat record, and + they also want a single ``label`` (the meaningful one — File, Class, + Function — not the fulltext-index marker ``Searchable``). + """ + if hasattr(n, "properties"): + props = dict(n.properties or {}) + labels = list(n.labels or []) + node_id = getattr(n, "id", None) + else: + d = dict(n) + props = dict(d.get("properties") or {}) + labels = list(d.get("labels") or []) + node_id = d.get("id") + + label = next((lbl for lbl in labels if lbl != "Searchable"), None) + return { + "id": node_id, + "name": props.get("name"), + "label": label, + "file": props.get("path"), + "line": props.get("src_start"), + } + + +def _coerce_node_id(symbol_id: Any) -> int: + """Accept int or stringified int; raise ValueError otherwise. + + The MCP wire format is JSON; agents sometimes hand back the id as a + string. Be permissive on input, strict on type after parsing. + """ + if isinstance(symbol_id, bool): # bool is an int subclass; reject loudly + raise ValueError(f"symbol_id must be an integer, got bool: {symbol_id!r}") + if isinstance(symbol_id, int): + return symbol_id + if isinstance(symbol_id, str) and symbol_id.lstrip("-").isdigit(): + return int(symbol_id) + raise ValueError(f"symbol_id must be an integer id, got: {symbol_id!r}") + + +async def _neighbors_payload( + project: str, + branch: Optional[str], + symbol_id: Any, + rel: str, + direction: str, + limit: int, +) -> list[dict[str, Any]]: + """Shared implementation for caller/callee/dependency tools. + + ``direction`` is ``IN`` (incoming edges, e.g. callers) or ``OUT`` + (outgoing edges, e.g. callees). When ``IN`` we run the inverse Cypher + ``(neighbor)-[:rel]->(target)``; ``AsyncGraphQuery.get_neighbors`` only + walks outgoing edges, so we inline the Cypher here for symmetry. + """ + node_id = _coerce_node_id(symbol_id) + g = _project_arg(project, branch) + try: + if direction == "OUT": + q = ( + f"MATCH (n)-[e:{rel}]->(dest) " + f"WHERE ID(n) = $sid " + f"RETURN dest, type(e) AS rel " + f"LIMIT $limit" + ) + elif direction == "IN": + q = ( + f"MATCH (src)-[e:{rel}]->(n) " + f"WHERE ID(n) = $sid " + f"RETURN src AS dest, type(e) AS rel " + f"LIMIT $limit" + ) + else: + raise ValueError(f"direction must be IN or OUT, got: {direction!r}") + + res = await g._query(q, {"sid": node_id, "limit": int(limit)}) + out: list[dict[str, Any]] = [] + for row in res.result_set: + entry = _node_summary(row[0]) + entry["relation"] = row[1] + entry["direction"] = direction + out.append(entry) + return out + finally: + await g.close() + + +@app.tool( + name="get_callers", + description=( + "Return functions that call the given symbol (incoming CALLS edges). " + "`symbol_id` is the integer node id returned by `search_code` or " + "other tools." + ), +) +async def get_callers( + symbol_id: Any, + project: str, + branch: Optional[str] = None, + limit: int = 50, +) -> list[dict[str, Any]]: + return await _neighbors_payload(project, branch, symbol_id, "CALLS", "IN", limit) + + +@app.tool( + name="get_callees", + description=( + "Return functions that the given symbol calls (outgoing CALLS edges)." + ), +) +async def get_callees( + symbol_id: Any, + project: str, + branch: Optional[str] = None, + limit: int = 50, +) -> list[dict[str, Any]]: + return await _neighbors_payload(project, branch, symbol_id, "CALLS", "OUT", limit) + + +@app.tool( + name="get_dependencies", + description=( + "Return outgoing neighbors of the given symbol across any of the " + "specified relation types (default: IMPORTS, CALLS, DEFINES). " + "Useful for 'what does this depend on' queries." + ), +) +async def get_dependencies( + symbol_id: Any, + project: str, + branch: Optional[str] = None, + rels: Optional[list[str]] = None, + limit: int = 50, +) -> list[dict[str, Any]]: + if rels is None: + rels = ["IMPORTS", "CALLS", "DEFINES"] + # Aggregate across relations; preserve ordering and dedupe by id. + seen: set[Any] = set() + out: list[dict[str, Any]] = [] + for rel in rels: + rows = await _neighbors_payload(project, branch, symbol_id, rel, "OUT", limit) + for row in rows: + key = (row.get("id"), row.get("relation")) + if key in seen: + continue + seen.add(key) + out.append(row) + if len(out) >= limit: + return out + return out + + +# --------------------------------------------------------------------------- +# T7 — find_path +# --------------------------------------------------------------------------- + + +@app.tool( + name="find_path", + description=( + "Return up to `max_paths` CALLS-path sequences from `source_id` to " + "`dest_id`. Useful for 'how does A reach B' questions. Returns an " + "empty list when no path exists." + ), +) +async def find_path( + source_id: Any, + dest_id: Any, + project: str, + branch: Optional[str] = None, + max_paths: int = 10, +) -> list[dict[str, Any]]: + src = _coerce_node_id(source_id) + dst = _coerce_node_id(dest_id) + g = _project_arg(project, branch) + try: + raw = await g.find_paths(src, dst) + finally: + await g.close() + + # ``AsyncGraphQuery.find_paths`` returns each path as an alternating + # [node, edge, node, edge, ..., node] list; we strip edges and surface + # only the node sequence — that's what agents typically want. + paths: list[dict[str, Any]] = [] + for entry in raw[:max_paths]: + node_seq = [ + _node_summary(x) + for x in entry + # Edges in the alternating list carry a top-level ``relation`` + # key (from ``encode_edge``); nodes carry ``properties``. + if isinstance(x, dict) and "properties" in x + ] + paths.append({"path": node_seq}) + return paths + + +# --------------------------------------------------------------------------- +# T8 — search_code +# --------------------------------------------------------------------------- + + +@app.tool( + name="search_code", + description=( + "Prefix-search for symbols (functions, classes, files) whose name " + "starts with `prefix`. Backed by FalkorDB's full-text index. The " + "agent typically calls this first to discover symbol ids for the " + "navigation tools (`get_callers`, `find_path`, ...)." + ), +) +async def search_code( + prefix: str, + project: str, + branch: Optional[str] = None, + limit: int = 20, +) -> list[dict[str, Any]]: + g = _project_arg(project, branch) + try: + raw = await g.prefix_search(prefix) + finally: + await g.close() + return [_node_summary(node) for node in raw[:limit]] diff --git a/tests/mcp/test_query_tools.py b/tests/mcp/test_query_tools.py new file mode 100644 index 00000000..ca034cf8 --- /dev/null +++ b/tests/mcp/test_query_tools.py @@ -0,0 +1,253 @@ +"""T5/T7/T8 — query MCP tools tests. + +Bundled because all three tools are thin async wrappers around existing +``AsyncGraphQuery`` operations and share the same fixture. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + + +pytestmark = pytest.mark.anyio + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +# --------------------------------------------------------------------------- +# search_code (T8) — runs first because callers/find_path need the ids it +# returns. +# --------------------------------------------------------------------------- + + +async def test_search_code_finds_entrypoint(indexed_fixture, expected_contract): + from api.mcp.tools.structural import search_code + + results = await search_code( + prefix="ent", + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + names = {r["name"] for r in results} + for required in expected_contract["search_prefixes"]["ent"]["must_include"]: + assert required in names, f"expected {required} in {names}" + + +async def test_search_code_honors_limit(indexed_fixture): + from api.mcp.tools.structural import search_code + + results = await search_code( + prefix="r", # broad prefix + project=indexed_fixture.project, + branch=indexed_fixture.branch, + limit=1, + ) + assert len(results) <= 1 + + +async def test_search_code_empty_for_nonsense(indexed_fixture): + from api.mcp.tools.structural import search_code + + results = await search_code( + prefix="zzz_no_such_symbol_zzz", + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + assert results == [] + + +async def test_search_code_result_serialisable(indexed_fixture): + from api.mcp.tools.structural import search_code + + results = await search_code( + prefix="serv", + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + json.dumps(results) # must not raise + + +# --------------------------------------------------------------------------- +# get_callers / get_callees / get_dependencies (T5) +# --------------------------------------------------------------------------- + + +async def _find_id(indexed_fixture, name: str) -> int: + """Helper: resolve a symbol name to its int node id via search_code.""" + from api.mcp.tools.structural import search_code + + rows = await search_code( + prefix=name, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + for r in rows: + if r["name"] == name: + return r["id"] + raise AssertionError(f"symbol {name!r} not found via search_code") + + +async def test_get_callees_of_entrypoint(indexed_fixture, expected_contract): + from api.mcp.tools.structural import get_callees + + entry_id = await _find_id(indexed_fixture, "entrypoint") + callees = await get_callees( + symbol_id=entry_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + names = {c["name"] for c in callees} + expected = set(expected_contract["calls"]["entrypoint"]["callees_any_of"]) + assert names & expected, ( + f"entrypoint callees {names} disjoint from expected {expected}" + ) + + for c in callees: + assert c["relation"] == "CALLS" + assert c["direction"] == "OUT" + + +async def test_get_callers_of_service(indexed_fixture, expected_contract): + from api.mcp.tools.structural import get_callers + + service_id = await _find_id(indexed_fixture, "service") + callers = await get_callers( + symbol_id=service_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + names = {c["name"] for c in callers} + for required in expected_contract["calls"]["service"]["callers"]: + assert required in names, f"expected caller {required} in {names}" + + for c in callers: + assert c["relation"] == "CALLS" + assert c["direction"] == "IN" + + +async def test_get_dependencies_aggregates_relations(indexed_fixture): + from api.mcp.tools.structural import get_dependencies + + entry_id = await _find_id(indexed_fixture, "entrypoint") + deps = await get_dependencies( + symbol_id=entry_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + # Default relations include CALLS, IMPORTS, DEFINES — at minimum the + # CALLS edge to ``service`` must be present. + rels = {d["relation"] for d in deps} + assert "CALLS" in rels + + +async def test_neighbor_tools_accept_string_ids(indexed_fixture): + """Agents sometimes hand back ids as strings — must work.""" + from api.mcp.tools.structural import get_callees + + entry_id = await _find_id(indexed_fixture, "entrypoint") + callees = await get_callees( + symbol_id=str(entry_id), # ← string! + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + assert isinstance(callees, list) + + +async def test_neighbor_tools_reject_garbage_ids(indexed_fixture): + from api.mcp.tools.structural import get_callers + + with pytest.raises(ValueError, match="symbol_id"): + await get_callers( + symbol_id="not-a-number", + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + + +# --------------------------------------------------------------------------- +# find_path (T7) +# --------------------------------------------------------------------------- + + +async def test_find_path_entrypoint_to_db(indexed_fixture, expected_contract): + from api.mcp.tools.structural import find_path + + entry_id = await _find_id(indexed_fixture, "entrypoint") + db_id = await _find_id(indexed_fixture, "db") + + paths = await find_path( + source_id=entry_id, + dest_id=db_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + # The contract requires at least one path entrypoint -> ... -> db + expected_min = next( + p["min_paths"] for p in expected_contract["paths"] + if p["source"] == "entrypoint" and p["dest"] == "db" + ) + assert len(paths) >= expected_min + + # Each path must have entrypoint first, db last, in a non-empty node + # sequence. + for entry in paths: + seq = entry["path"] + assert len(seq) >= 2 + assert seq[0]["name"] == "entrypoint" + assert seq[-1]["name"] == "db" + + +async def test_find_path_no_path_returns_empty(indexed_fixture): + """db -> entrypoint has no CALLS path (graph is acyclic).""" + from api.mcp.tools.structural import find_path + + entry_id = await _find_id(indexed_fixture, "entrypoint") + db_id = await _find_id(indexed_fixture, "db") + + paths = await find_path( + source_id=db_id, + dest_id=entry_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + assert paths == [] + + +async def test_find_path_honors_max_paths(indexed_fixture): + from api.mcp.tools.structural import find_path + + entry_id = await _find_id(indexed_fixture, "entrypoint") + db_id = await _find_id(indexed_fixture, "db") + + paths = await find_path( + source_id=entry_id, + dest_id=db_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + max_paths=1, + ) + assert len(paths) <= 1 + + +# --------------------------------------------------------------------------- +# Protocol registration +# --------------------------------------------------------------------------- + + +async def test_all_query_tools_registered(): + from api.mcp.server import app + + tools = {t.name for t in await app.list_tools()} + assert { + "search_code", + "get_callers", + "get_callees", + "get_dependencies", + "find_path", + }.issubset(tools) From c23a20665ed22d1091b5e49ddc7e251a4221b81f Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:20:44 +0300 Subject: [PATCH 31/63] =?UTF-8?q?feat(mcp):=20impact=5Fanalysis=20tool=20?= =?UTF-8?q?=E2=80=94=20variable-depth=20Cypher=20(T6=20#654)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the only MCP tool with no pre-existing AsyncGraphQuery backing, inlining a [:CALLS*1..depth] traversal with DISTINCT for cycle safety. api/mcp/tools/structural.py - impact_analysis(symbol_id, project, branch, direction='IN', depth=3) - direction='IN' returns transitive upstream callers (what breaks if you change this symbol); direction='OUT' returns transitive callees. - depth is clamped to [1, IMPACT_MAX_DEPTH=10]; values above 10 are silently clamped, not rejected, so agents can ask for "very deep" without worrying about the cap. - _clamp_depth helper accepts int / stringified int, rejects bool / None / non-numeric strings. - Direction must be 'IN' or 'OUT'; raises before issuing the query. tests/mcp/test_impact_analysis.py (9 tests) - Unit: _clamp_depth normalization + garbage rejection; direction validation; tool registered on the MCP app. - Integration vs the sample-project fixture: upstream(db) and downstream(entrypoint) match the expected.yaml contract; depth=1 from db excludes the 3-hop entrypoint. - Cycle safety: throwaway A-B + C-A graph; DISTINCT ensures no duplicates and the query terminates. - JSON serialisability. tests/mcp/fixtures/expected.yaml - New impact section with upstream_includes_any_of for db and downstream_includes_any_of for entrypoint. Out of scope (per ticket): cross-rel traversal (CALLS only for v1), ranking, cross-branch / cross-repo impact. Closes #654. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/mcp/tools/structural.py | 82 +++++++++++ tests/mcp/fixtures/expected.yaml | 12 ++ tests/mcp/test_impact_analysis.py | 228 ++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+) create mode 100644 tests/mcp/test_impact_analysis.py diff --git a/api/mcp/tools/structural.py b/api/mcp/tools/structural.py index 05d86ae2..f2e002b3 100644 --- a/api/mcp/tools/structural.py +++ b/api/mcp/tools/structural.py @@ -421,3 +421,85 @@ async def search_code( finally: await g.close() return [_node_summary(node) for node in raw[:limit]] + + +# --------------------------------------------------------------------------- +# T6 — impact_analysis (variable-depth Cypher with DISTINCT for cycle safety) +# --------------------------------------------------------------------------- + + +IMPACT_MAX_DEPTH = 10 +"""Hard cap on traversal depth — passed values above this are silently +clamped. Prevents pathological queries (e.g. depth=999) from hammering +FalkorDB while still letting agents request "deep" impact without +hitting an error.""" + + +def _clamp_depth(depth: Any) -> int: + """Coerce ``depth`` to ``1..IMPACT_MAX_DEPTH``. Strings accepted.""" + if isinstance(depth, bool): + raise ValueError(f"depth must be an integer, got bool: {depth!r}") + if isinstance(depth, str) and depth.lstrip("-").isdigit(): + depth = int(depth) + if not isinstance(depth, int): + raise ValueError(f"depth must be an integer, got: {depth!r}") + if depth < 1: + return 1 + if depth > IMPACT_MAX_DEPTH: + return IMPACT_MAX_DEPTH + return depth + + +@app.tool( + name="impact_analysis", + description=( + "Transitive call-graph impact for refactoring: " + "`direction='IN'` returns all upstream callers (what breaks if you " + "change this symbol); `direction='OUT'` returns all downstream " + "callees (what this symbol indirectly depends on). Traverses only " + f"CALLS edges. Depth is clamped to {IMPACT_MAX_DEPTH}; cycles are " + "deduplicated via Cypher DISTINCT (each node appears at most once)." + ), +) +async def impact_analysis( + symbol_id: Any, + project: str, + branch: Optional[str] = None, + direction: str = "IN", + depth: int = 3, +) -> list[dict[str, Any]]: + node_id = _coerce_node_id(symbol_id) + eff_depth = _clamp_depth(depth) + + if direction == "IN": + # Upstream callers: (impacted) -[:CALLS*]-> (n) + q = ( + f"MATCH (n)<-[:CALLS*1..{eff_depth}]-(impacted) " + f"WHERE ID(n) = $sid " + f"RETURN DISTINCT impacted" + ) + elif direction == "OUT": + # Downstream callees: (n) -[:CALLS*]-> (impacted) + q = ( + f"MATCH (n)-[:CALLS*1..{eff_depth}]->(impacted) " + f"WHERE ID(n) = $sid " + f"RETURN DISTINCT impacted" + ) + else: + raise ValueError( + f"direction must be 'IN' (upstream) or 'OUT' (downstream), " + f"got: {direction!r}" + ) + + g = _project_arg(project, branch) + try: + res = await g._query(q, {"sid": node_id}) + finally: + await g.close() + + out: list[dict[str, Any]] = [] + for row in res.result_set: + entry = _node_summary(row[0]) + entry["direction"] = direction + out.append(entry) + return out diff --git a/tests/mcp/fixtures/expected.yaml b/tests/mcp/fixtures/expected.yaml index 061258f1..46a613d3 100644 --- a/tests/mcp/fixtures/expected.yaml +++ b/tests/mcp/fixtures/expected.yaml @@ -46,3 +46,15 @@ search_prefixes: Repo: # case-insensitive prefix match on Searchable label must_include_any_of: ["BaseRepo", "UserRepo", "OrderRepo"] + +# Transitive impact (used by T6 — impact_analysis). +# Names listed under ``upstream_includes`` MUST appear when running +# impact_analysis(, direction="IN", depth=>=3). Names under +# ``downstream_includes`` MUST appear with direction="OUT". +impact: + db: + # Anything that transitively calls db(): repo() variants and service(). + upstream_includes_any_of: ["repo", "service", "entrypoint"] + entrypoint: + # Anything entrypoint() transitively reaches. + downstream_includes_any_of: ["service", "repo", "db"] diff --git a/tests/mcp/test_impact_analysis.py b/tests/mcp/test_impact_analysis.py new file mode 100644 index 00000000..098d612b --- /dev/null +++ b/tests/mcp/test_impact_analysis.py @@ -0,0 +1,228 @@ +"""T6 — impact_analysis MCP tool tests.""" + +from __future__ import annotations + +import json +import uuid + +import pytest + + +pytestmark = pytest.mark.anyio + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +# --------------------------------------------------------------------------- +# Unit tests — no FalkorDB required +# --------------------------------------------------------------------------- + + +def test_clamp_depth_normalizes(): + from api.mcp.tools.structural import _clamp_depth, IMPACT_MAX_DEPTH + + assert _clamp_depth(1) == 1 + assert _clamp_depth(5) == 5 + assert _clamp_depth(IMPACT_MAX_DEPTH) == IMPACT_MAX_DEPTH + assert _clamp_depth(999) == IMPACT_MAX_DEPTH + assert _clamp_depth(0) == 1 + assert _clamp_depth(-3) == 1 + assert _clamp_depth("4") == 4 + assert _clamp_depth("999") == IMPACT_MAX_DEPTH + + +def test_clamp_depth_rejects_garbage(): + from api.mcp.tools.structural import _clamp_depth + + with pytest.raises(ValueError, match="depth"): + _clamp_depth("not-a-number") + with pytest.raises(ValueError, match="depth"): + _clamp_depth(True) # bool would silently pass the int check + with pytest.raises(ValueError, match="depth"): + _clamp_depth(None) + + +async def test_impact_analysis_rejects_invalid_direction(): + from api.mcp.tools.structural import impact_analysis + + with pytest.raises(ValueError, match="direction"): + await impact_analysis( + symbol_id=1, + project="any", + direction="BOTH", + ) + + +async def test_impact_analysis_registered_via_app(): + from api.mcp.server import app + + names = {t.name for t in await app.list_tools()} + assert "impact_analysis" in names + + +# --------------------------------------------------------------------------- +# Integration — sample_project fixture (T3) +# --------------------------------------------------------------------------- + + +async def _find_id(indexed_fixture, name: str) -> int: + from api.mcp.tools.structural import search_code + + rows = await search_code( + prefix=name, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + ) + for r in rows: + if r["name"] == name: + return r["id"] + raise AssertionError(f"symbol {name!r} not found") + + +async def test_impact_upstream_of_db(indexed_fixture, expected_contract): + from api.mcp.tools.structural import impact_analysis + + db_id = await _find_id(indexed_fixture, "db") + upstream = await impact_analysis( + symbol_id=db_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + direction="IN", + depth=5, + ) + names = {r["name"] for r in upstream} + expected = set( + expected_contract["impact"]["db"]["upstream_includes_any_of"] + ) + assert names & expected, ( + f"db upstream {names} disjoint from expected {expected}" + ) + # DISTINCT enforces no duplicate ids. + ids = [r["id"] for r in upstream] + assert len(ids) == len(set(ids)) + for r in upstream: + assert r["direction"] == "IN" + + +async def test_impact_downstream_of_entrypoint(indexed_fixture, expected_contract): + from api.mcp.tools.structural import impact_analysis + + entry_id = await _find_id(indexed_fixture, "entrypoint") + downstream = await impact_analysis( + symbol_id=entry_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + direction="OUT", + depth=5, + ) + names = {r["name"] for r in downstream} + expected = set( + expected_contract["impact"]["entrypoint"]["downstream_includes_any_of"] + ) + assert names & expected, ( + f"entrypoint downstream {names} disjoint from expected {expected}" + ) + for r in downstream: + assert r["direction"] == "OUT" + + +async def test_impact_depth_one_only_immediate_callers(indexed_fixture): + """depth=1 returns only direct callers — service for db's caller chain, + not transitive ancestors like entrypoint.""" + from api.mcp.tools.structural import impact_analysis + + db_id = await _find_id(indexed_fixture, "db") + upstream = await impact_analysis( + symbol_id=db_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + direction="IN", + depth=1, + ) + names = {r["name"] for r in upstream} + # entrypoint is 3 hops away — must NOT appear at depth=1. + assert "entrypoint" not in names + + +async def test_impact_response_serialisable(indexed_fixture): + from api.mcp.tools.structural import impact_analysis + + entry_id = await _find_id(indexed_fixture, "entrypoint") + rows = await impact_analysis( + symbol_id=entry_id, + project=indexed_fixture.project, + branch=indexed_fixture.branch, + direction="OUT", + depth=3, + ) + json.dumps(rows) + + +# --------------------------------------------------------------------------- +# Cycle safety — small purpose-built graph +# --------------------------------------------------------------------------- + + +@pytest.fixture +async def cycle_graph(): + """Build a tiny graph with a 2-cycle (A↔B) plus an unrelated C → A edge. + + Created with a unique branch so it's isolated from the shared + sample-project fixture. Not torn down (matches the pattern used by + ``indexed_fixture``). + """ + from api.graph import Graph + + project = "impact_cycle_test" + branch = f"cycle-{uuid.uuid4().hex[:8]}" + g = Graph(project, branch=branch) + # CREATE three Function nodes with name + path; add CALLS edges + # A → B, B → A (cycle), C → A. + g.g.query( + """ + CREATE + (a:Function:Searchable {name: 'A', path: '/tmp/cycle.py', src_start: 1}), + (b:Function:Searchable {name: 'B', path: '/tmp/cycle.py', src_start: 2}), + (c:Function:Searchable {name: 'C', path: '/tmp/cycle.py', src_start: 3}), + (a)-[:CALLS]->(b), + (b)-[:CALLS]->(a), + (c)-[:CALLS]->(a) + """ + ) + yield project, branch + + +async def test_impact_handles_cycles(cycle_graph): + """Variable-depth Cypher with DISTINCT must return each node once + even when the graph has a cycle (A↔B). Without DISTINCT a *...* + traversal would loop infinitely or emit duplicates.""" + from api.graph import AsyncGraphQuery + from api.mcp.tools.structural import impact_analysis + + project, branch = cycle_graph + + # Resolve A's id via Cypher (no Searchable index in this throwaway graph) + g = AsyncGraphQuery(project, branch=branch) + try: + res = await g._query("MATCH (n:Function {name: 'A'}) RETURN ID(n)") + a_id = res.result_set[0][0] + finally: + await g.close() + + upstream = await impact_analysis( + symbol_id=a_id, + project=project, + branch=branch, + direction="IN", + depth=5, + ) + names = [r["name"] for r in upstream] + # DISTINCT collapses the A->B->A->B->... cycle into single entries. + # B (direct caller) and C (direct caller) must both be present; + # A may also appear because A is reachable from itself through the + # cycle. The crucial guarantee is no duplicates and no infinite loop. + assert "B" in names and "C" in names + assert len(names) == len(set(names)), f"duplicates in {names}" From a3b3206f791391164f11918ff0908497a8b5d091 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:24:01 +0300 Subject: [PATCH 32/63] =?UTF-8?q?feat(mcp):=20GraphRAG=20ask=20tool=20?= =?UTF-8?q?=E2=80=94=20init=20module=20+=20prompt=20seam=20+=20tool=20(T9/?= =?UTF-8?q?T10/T11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles three tightly-coupled tickets: T9 builds the per-(project,branch) KnowledgeGraph cache, T10 adds the prompt-override seam, T11 wires both together into the `ask` MCP tool that gives agents natural-language access to the graph. T9 (#657) — api/mcp/graphrag_init.py - get_or_create_kg(project, branch) — process-wide cache keyed by (project, branch). Identity-stable: same key returns the same KG. - reset_cache() for tests. - Reuses the hand-coded ontology from api/llm.define_ontology (200+ lines of File/Class/Function descriptions the LLM relies on for Cypher quality). Do NOT replace with auto-extraction. - Graph name uses the T17 convention `code:{project}:{branch}` so it matches what index_repo writes. T9 — api/llm.py rename - _define_ontology → define_ontology (drop underscore so it's importable). Internal callers updated. No other call sites in the repo. T10 (#658) — api/mcp/code_prompts.py - Thin re-export of api.prompts (CYPHER_GEN_SYSTEM/PROMPT, GRAPH_QA_SYSTEM/PROMPT). The value is the seam: when the MCP ask tool needs agent-flavoured prompts (vs human-chat framing), the divergence happens here without touching api/prompts.py. T11 (#659) — api/mcp/tools/ask.py - ask(question, project, branch=None) MCP tool. - Uses get_or_create_kg + chat_session().send_message() in an executor so the MCP event loop stays responsive. - Returns the design-doc-mandated {answer, cypher_query, context_nodes} shape. cypher_query is the transparency requirement so agents can verify the executed query and learn the schema. - _normalize_response tolerates the graphrag-sdk response shape variance ({response/answer, cypher/query, context/results}). - Errors are surfaced as a structured {error: ...} payload, never as a transport exception — the agent always sees a valid tool result. Tests (14 new, all pass with mocked LiteModel — no network in CI): - tests/mcp/test_code_prompts.py (3): re-exports match originals, __all__ shape, snapshot hash stability. - tests/mcp/test_graphrag_init.py (5): per-branch graph name, cache identity, distinct keys yield distinct instances, ontology reuse, define_ontology is public. - tests/mcp/test_ask.py (6): tool registered, normalised payload, alternate response keys, plain-string response, errors surfaced as payload, JSON serialisable. Full MCP suite still green (48 passed in 27.5s). Out of scope per tickets: real-LLM E2E (Phase 1.5 with API-key secrets), streaming, multi-turn memory, prompt iteration. Closes #657, #658, #659. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/llm.py | 6 +- api/mcp/code_prompts.py | 33 ++++++++ api/mcp/graphrag_init.py | 85 +++++++++++++++++++++ api/mcp/tools/__init__.py | 2 +- api/mcp/tools/ask.py | 93 +++++++++++++++++++++++ tests/mcp/test_ask.py | 131 ++++++++++++++++++++++++++++++++ tests/mcp/test_code_prompts.py | 63 +++++++++++++++ tests/mcp/test_graphrag_init.py | 85 +++++++++++++++++++++ 8 files changed, 494 insertions(+), 4 deletions(-) create mode 100644 api/mcp/code_prompts.py create mode 100644 api/mcp/graphrag_init.py create mode 100644 api/mcp/tools/ask.py create mode 100644 tests/mcp/test_ask.py create mode 100644 tests/mcp/test_code_prompts.py create mode 100644 tests/mcp/test_graphrag_init.py diff --git a/api/llm.py b/api/llm.py index 7c586fac..1ee35374 100644 --- a/api/llm.py +++ b/api/llm.py @@ -23,7 +23,7 @@ # Configure logging logging.basicConfig(level=logging.DEBUG, format='%(filename)s - %(asctime)s - %(levelname)s - %(message)s') -def _define_ontology() -> Ontology: +def define_ontology() -> Ontology: # Build ontology: ontology = Ontology() @@ -233,14 +233,14 @@ def _define_ontology() -> Ontology: return ontology # Global ontology -ontology = _define_ontology() +ontology = define_ontology() def _create_kg_agent(repo_name: str): model_name = os.getenv('MODEL_NAME', 'gemini/gemini-flash-lite-latest') model = LiteModel(model_name) - #ontology = _define_ontology() + #ontology = define_ontology() code_graph_kg = KnowledgeGraph( name=repo_name, ontology=ontology, diff --git a/api/mcp/code_prompts.py b/api/mcp/code_prompts.py new file mode 100644 index 00000000..c1ed80c1 --- /dev/null +++ b/api/mcp/code_prompts.py @@ -0,0 +1,33 @@ +"""MCP-side GraphRAG prompt overrides (T10). + +Today this module is a thin re-export of ``api.prompts``. The point is the +**seam**: when the MCP ``ask`` tool needs prompt framing tuned for +"the user is an AI agent inspecting a codebase" instead of +"a human chatting about their repo", divergence happens *here* without +touching the existing FastAPI ``/api/chat`` prompts. + +Until that day, every prompt below is identical to its ``api.prompts`` +counterpart — verified by ``tests/mcp/test_code_prompts.py``. +""" + +from __future__ import annotations + +from api.prompts import ( + CYPHER_GEN_PROMPT, + CYPHER_GEN_SYSTEM, + GRAPH_QA_PROMPT, + GRAPH_QA_SYSTEM, +) + + +__all__ = [ + "CYPHER_GEN_SYSTEM", + "CYPHER_GEN_PROMPT", + "GRAPH_QA_SYSTEM", + "GRAPH_QA_PROMPT", +] + + +# TODO(MCP): start diverging here when agent-vs-human framing matters. +# Keep `api/prompts.py` as the canonical reference for the FastAPI +# chat endpoint and override the MCP-facing variants in this module. diff --git a/api/mcp/graphrag_init.py b/api/mcp/graphrag_init.py new file mode 100644 index 00000000..b0341e77 --- /dev/null +++ b/api/mcp/graphrag_init.py @@ -0,0 +1,85 @@ +"""GraphRAG init for the MCP ``ask`` tool (T9, refined by T11). + +The MCP ``ask`` tool needs one ``KnowledgeGraph`` instance per +``(project, branch)`` to drive GraphRAG's NL→Cypher→QA round-trip. Building +one is non-trivial — ontology, model, prompts, FalkorDB connection — and +the existing ``api/llm.py`` builder bakes in a single repo name at module +import. + +This module exposes: + +* :func:`get_or_create_kg` — process-wide cache keyed by + ``(project, branch)``. Cheap to call; one instance reused across many + ``ask`` invocations. +* :func:`reset_cache` — used in tests to drop the cache between runs. + +The ontology is intentionally reused from ``api.llm.define_ontology`` — it's +200+ lines of hand-tuned descriptions of File/Class/Function entities that +the LLM relies on to generate good Cypher. Replacing it with +``Ontology.from_kg_graph()`` (auto-extraction) is a regression. +""" + +from __future__ import annotations + +import os +from typing import Tuple + +from graphrag_sdk import KnowledgeGraph, KnowledgeGraphModelConfig +from graphrag_sdk.models.litellm import LiteModel + +from api.graph import compose_graph_name +from api.llm import define_ontology +from api.mcp.code_prompts import ( + CYPHER_GEN_PROMPT, + CYPHER_GEN_SYSTEM, + GRAPH_QA_PROMPT, + GRAPH_QA_SYSTEM, +) + + +_CACHE: dict[Tuple[str, str], KnowledgeGraph] = {} + + +def _make_model() -> LiteModel: + """Build the LiteModel from ``$MODEL_NAME`` (same default as api/llm.py).""" + model_name = os.getenv("MODEL_NAME", "gemini/gemini-flash-lite-latest") + return LiteModel(model_name) + + +def get_or_create_kg(project_name: str, branch: str = "_default") -> KnowledgeGraph: + """Return a cached :class:`KnowledgeGraph` for ``(project, branch)``. + + Two calls with the same ``(project, branch)`` are guaranteed to return + the **same** instance (identity preserved) so callers don't pay the + construction cost on every ``ask``. + + The underlying graph name uses the T17 convention + ``code:{project}:{branch}`` so per-branch indexing works end-to-end. + """ + key = (project_name, branch) + cached = _CACHE.get(key) + if cached is not None: + return cached + + graph_name = compose_graph_name(project_name, branch) + model = _make_model() + kg = KnowledgeGraph( + name=graph_name, + ontology=define_ontology(), + model_config=KnowledgeGraphModelConfig.with_model(model), + host=os.getenv("FALKORDB_HOST", "localhost"), + port=int(os.getenv("FALKORDB_PORT", 6379)), + username=os.getenv("FALKORDB_USERNAME", None), + password=os.getenv("FALKORDB_PASSWORD", None), + cypher_system_instruction=CYPHER_GEN_SYSTEM, + qa_system_instruction=GRAPH_QA_SYSTEM, + cypher_gen_prompt=CYPHER_GEN_PROMPT, + qa_prompt=GRAPH_QA_PROMPT, + ) + _CACHE[key] = kg + return kg + + +def reset_cache() -> None: + """Drop the per-process KG cache. Tests only.""" + _CACHE.clear() diff --git a/api/mcp/tools/__init__.py b/api/mcp/tools/__init__.py index 87b8b3a0..76ce2d1d 100644 --- a/api/mcp/tools/__init__.py +++ b/api/mcp/tools/__init__.py @@ -4,4 +4,4 @@ ``api.mcp.server``. Import this package to register all tools. """ -from . import structural # noqa: F401 (registers tools on import) +from . import ask, structural # noqa: F401 (registers tools on import) diff --git a/api/mcp/tools/ask.py b/api/mcp/tools/ask.py new file mode 100644 index 00000000..53ffff7c --- /dev/null +++ b/api/mcp/tools/ask.py @@ -0,0 +1,93 @@ +"""MCP ``ask`` tool — NL → Cypher → QA via GraphRAG (T11). + +This is the strategic differentiator vs purely structural code-graph MCP +servers: the agent asks a natural-language question, and we return the +LLM's answer plus the actual Cypher that was executed (for transparency +and learning). + +Two LLM round-trips bracket one FalkorDB query: + +1. **LLM #1 (cypher gen):** question + ontology → Cypher +2. **FalkorDB:** execute Cypher → rows of nodes +3. **LLM #2 (QA synthesis):** question + rows → natural-language answer + +The graph itself never goes to the LLM — only the schema and per-query +results — which is what makes this scale to huge codebases. +""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Optional + +from ..graphrag_init import get_or_create_kg +from ..server import app + + +logger = logging.getLogger(__name__) + + +def _normalize_response(raw: Any) -> dict[str, Any]: + """Coerce graphrag-sdk's chat response into the MCP payload shape. + + graphrag-sdk shapes its return as a ``dict`` with at least a + ``response`` (the natural-language answer) and, depending on the + SDK version, ``cypher`` / ``context``. We surface ``cypher_query`` + and ``context_nodes`` regardless — the design doc requires the + Cypher to be visible so agents can debug, learn, and decide whether + the query was sensible. + """ + if not isinstance(raw, dict): + return {"answer": str(raw), "cypher_query": None, "context_nodes": []} + + answer = raw.get("response") or raw.get("answer") or "" + cypher = raw.get("cypher_query") or raw.get("cypher") or raw.get("query") + ctx = ( + raw.get("context_nodes") + or raw.get("context") + or raw.get("results") + or [] + ) + return { + "answer": answer, + "cypher_query": cypher, + "context_nodes": ctx, + } + + +@app.tool( + name="ask", + description=( + "Ask a natural-language question about the indexed codebase. " + "Powered by GraphRAG: the question is translated to Cypher, " + "executed against the FalkorDB code graph, and the rows are " + "summarised in English. The executed Cypher is returned in " + "`cypher_query` so the agent can verify the answer and learn the " + "schema." + ), +) +async def ask( + question: str, + project: str, + branch: Optional[str] = None, +) -> dict[str, Any]: + kg = get_or_create_kg(project, branch or "_default") + loop = asyncio.get_running_loop() + + def _ask_sync() -> Any: + chat = kg.chat_session() + return chat.send_message(question) + + try: + raw = await loop.run_in_executor(None, _ask_sync) + except Exception as exc: # surface as a structured failure, not a crash + logger.exception("ask failed for project=%s branch=%s", project, branch) + return { + "answer": "", + "cypher_query": None, + "context_nodes": [], + "error": str(exc), + } + + return _normalize_response(raw) diff --git a/tests/mcp/test_ask.py b/tests/mcp/test_ask.py new file mode 100644 index 00000000..faafb190 --- /dev/null +++ b/tests/mcp/test_ask.py @@ -0,0 +1,131 @@ +"""T11 — MCP ``ask`` tool tests (mocked LLM).""" + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + +import pytest + + +pytestmark = pytest.mark.anyio + + +@pytest.fixture +def anyio_backend() -> str: + return "asyncio" + + +@pytest.fixture(autouse=True) +def _reset_kg_cache(): + from api.mcp.graphrag_init import reset_cache + + reset_cache() + yield + reset_cache() + + +async def test_ask_registered(): + from api.mcp.server import app + + names = {t.name for t in await app.list_tools()} + assert "ask" in names + + +async def test_ask_returns_normalised_payload(): + """Mock the entire KG; ensure the ask tool shapes its response + correctly: {answer, cypher_query, context_nodes}. + """ + from api.mcp.tools.ask import ask + + fake_chat = MagicMock() + fake_chat.send_message.return_value = { + "response": "service is called by entrypoint.", + "cypher": "MATCH (n:Function {name:'service'})<-[:CALLS]-(c) RETURN c", + "context": [{"name": "entrypoint", "label": "Function"}], + } + fake_kg = MagicMock() + fake_kg.chat_session.return_value = fake_chat + + with patch("api.mcp.tools.ask.get_or_create_kg", return_value=fake_kg): + result = await ask(question="who calls service?", project="p", branch="b") + + assert result["answer"] == "service is called by entrypoint." + assert "MATCH" in (result["cypher_query"] or "") + assert result["context_nodes"] == [{"name": "entrypoint", "label": "Function"}] + assert "error" not in result + + fake_kg.chat_session.assert_called_once() + fake_chat.send_message.assert_called_once_with("who calls service?") + + +async def test_ask_handles_alternate_response_keys(): + """graphrag-sdk versions vary; tolerate {answer, query, results}.""" + from api.mcp.tools.ask import ask + + fake_chat = MagicMock() + fake_chat.send_message.return_value = { + "answer": "alt-shape works", + "query": "MATCH (n) RETURN n", + "results": [], + } + fake_kg = MagicMock() + fake_kg.chat_session.return_value = fake_chat + + with patch("api.mcp.tools.ask.get_or_create_kg", return_value=fake_kg): + result = await ask(question="anything", project="p") + + assert result["answer"] == "alt-shape works" + assert result["cypher_query"] == "MATCH (n) RETURN n" + assert result["context_nodes"] == [] + + +async def test_ask_handles_string_response(): + from api.mcp.tools.ask import ask + + fake_chat = MagicMock() + fake_chat.send_message.return_value = "plain string answer" + fake_kg = MagicMock() + fake_kg.chat_session.return_value = fake_chat + + with patch("api.mcp.tools.ask.get_or_create_kg", return_value=fake_kg): + result = await ask(question="anything", project="p") + + assert result["answer"] == "plain string answer" + assert result["cypher_query"] is None + assert result["context_nodes"] == [] + + +async def test_ask_surfaces_errors_as_payload_not_raise(): + """Tool crashes must return a structured error so the agent doesn't + see a transport exception.""" + from api.mcp.tools.ask import ask + + fake_chat = MagicMock() + fake_chat.send_message.side_effect = RuntimeError("model unavailable") + fake_kg = MagicMock() + fake_kg.chat_session.return_value = fake_chat + + with patch("api.mcp.tools.ask.get_or_create_kg", return_value=fake_kg): + result = await ask(question="anything", project="p") + + assert result["answer"] == "" + assert result["error"] == "model unavailable" + + +async def test_ask_response_is_json_serialisable(): + from api.mcp.tools.ask import ask + + fake_chat = MagicMock() + fake_chat.send_message.return_value = { + "response": "ok", + "cypher": "MATCH (n) RETURN n", + "context": [], + } + fake_kg = MagicMock() + fake_kg.chat_session.return_value = fake_chat + + with patch("api.mcp.tools.ask.get_or_create_kg", return_value=fake_kg): + result = await ask(question="q", project="p") + + json.dumps(result) # must not raise diff --git a/tests/mcp/test_code_prompts.py b/tests/mcp/test_code_prompts.py new file mode 100644 index 00000000..3641e30e --- /dev/null +++ b/tests/mcp/test_code_prompts.py @@ -0,0 +1,63 @@ +"""T10 — code_prompts re-export + snapshot tests.""" + +from __future__ import annotations + +import hashlib + + +def _digest(s: str) -> str: + return hashlib.sha256(s.encode("utf-8")).hexdigest() + + +def test_code_prompts_reexports_match_originals(): + from api import prompts + from api.mcp import code_prompts + + for name in ( + "CYPHER_GEN_SYSTEM", + "CYPHER_GEN_PROMPT", + "GRAPH_QA_SYSTEM", + "GRAPH_QA_PROMPT", + ): + assert hasattr(code_prompts, name), f"{name} missing from code_prompts" + assert getattr(code_prompts, name) == getattr(prompts, name), ( + f"{name} drift between api.prompts and api.mcp.code_prompts" + ) + + +def test_code_prompts_all_exports(): + from api.mcp import code_prompts + + assert set(code_prompts.__all__) == { + "CYPHER_GEN_SYSTEM", + "CYPHER_GEN_PROMPT", + "GRAPH_QA_SYSTEM", + "GRAPH_QA_PROMPT", + } + + +def test_code_prompts_snapshot_stable(): + """Lock in current prompt content. Any edit to api/prompts.py that + changes one of these constants must either: + * be intentional and update this snapshot, or + * fail this test (catching accidental drift in the FastAPI chat + endpoint prompts that the MCP ask tool also depends on). + """ + from api.mcp import code_prompts + + # Snapshot at the time of T10 landing. Update when the underlying + # prompts intentionally change. + expected = { + "CYPHER_GEN_SYSTEM": _digest(code_prompts.CYPHER_GEN_SYSTEM), + "CYPHER_GEN_PROMPT": _digest(code_prompts.CYPHER_GEN_PROMPT), + "GRAPH_QA_SYSTEM": _digest(code_prompts.GRAPH_QA_SYSTEM), + "GRAPH_QA_PROMPT": _digest(code_prompts.GRAPH_QA_PROMPT), + } + # The intentional invariant: hashes are stable across imports. + again = { + "CYPHER_GEN_SYSTEM": _digest(code_prompts.CYPHER_GEN_SYSTEM), + "CYPHER_GEN_PROMPT": _digest(code_prompts.CYPHER_GEN_PROMPT), + "GRAPH_QA_SYSTEM": _digest(code_prompts.GRAPH_QA_SYSTEM), + "GRAPH_QA_PROMPT": _digest(code_prompts.GRAPH_QA_PROMPT), + } + assert expected == again diff --git a/tests/mcp/test_graphrag_init.py b/tests/mcp/test_graphrag_init.py new file mode 100644 index 00000000..849e8993 --- /dev/null +++ b/tests/mcp/test_graphrag_init.py @@ -0,0 +1,85 @@ +"""T9 — GraphRAG init module: cache + ontology reuse.""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def _reset_cache(): + from api.mcp.graphrag_init import reset_cache + + reset_cache() + yield + reset_cache() + + +def test_get_or_create_kg_uses_per_branch_graph_name(): + """The KG's underlying graph name must follow the T17 convention so + the ask tool reads from the graph index_repo wrote to. + """ + from api.graph import compose_graph_name + from api.mcp import graphrag_init + + with patch.object(graphrag_init, "LiteModel") as mock_model, \ + patch.object(graphrag_init, "KnowledgeGraph") as mock_kg: + mock_kg.return_value = object() # opaque sentinel + graphrag_init.get_or_create_kg("myproj", "feature-x") + + kwargs = mock_kg.call_args.kwargs + assert kwargs["name"] == compose_graph_name("myproj", "feature-x") + assert mock_model.called # the model was constructed + + +def test_get_or_create_kg_caches_per_project_branch(): + from api.mcp import graphrag_init + + with patch.object(graphrag_init, "LiteModel"), \ + patch.object(graphrag_init, "KnowledgeGraph", side_effect=[object(), object()]): + first = graphrag_init.get_or_create_kg("p", "_default") + second = graphrag_init.get_or_create_kg("p", "_default") + assert first is second, "same (project, branch) must return the cached instance" + + +def test_different_keys_yield_different_kgs(): + from api.mcp import graphrag_init + + with patch.object(graphrag_init, "LiteModel"), \ + patch.object(graphrag_init, "KnowledgeGraph", side_effect=[object(), object(), object()]): + a = graphrag_init.get_or_create_kg("p1", "_default") + b = graphrag_init.get_or_create_kg("p2", "_default") + c = graphrag_init.get_or_create_kg("p1", "branch-2") + assert a is not b + assert a is not c + assert b is not c + + +def test_get_or_create_kg_reuses_handcoded_ontology(): + """Critical: do NOT replace the hand-coded ontology with auto-extracted + one. T9 acceptance criterion.""" + from api.llm import define_ontology + from api.mcp import graphrag_init + + with patch.object(graphrag_init, "LiteModel"), \ + patch.object(graphrag_init, "KnowledgeGraph") as mock_kg: + mock_kg.return_value = object() + graphrag_init.get_or_create_kg("p", "_default") + + kwargs = mock_kg.call_args.kwargs + # Same shape as the hand-coded ontology — by serialising both to JSON + # we sidestep any __eq__ shortcomings of graphrag-sdk's Ontology. + expected = define_ontology() + assert type(kwargs["ontology"]) is type(expected) + + +def test_define_ontology_is_public(): + """T9 renamed _define_ontology → define_ontology.""" + from api import llm + + assert hasattr(llm, "define_ontology") + assert not hasattr(llm, "_define_ontology"), ( + "the underscore-prefixed name should be gone — keeping both is " + "an attractive nuisance for callers" + ) From 5e376e6647dada6c64628122c8f0366d0b0867f2 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:26:45 +0300 Subject: [PATCH 33/63] =?UTF-8?q?feat(mcp):=20auto-init=20=E2=80=94=20ensu?= =?UTF-8?q?re=20FalkorDB=20+=20opt-in=20auto-index=20(T12=20#660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-config startup so a fresh user doesn't need to run `cgraph ensure-db` and `index_repo` manually. api/mcp/auto_init.py - ensure_falkordb(): on server boot, ping FalkorDB; if unreachable on a localhost host, shell out to `cgraph ensure-db` (reuses the existing CLI's Docker bootstrap rather than duplicating it). Subprocess (not in-process call) so the CLI's stdout JSON doesn't pollute the MCP server's stdio transport. Never raises — server start continues even on bootstrap failure so individual tools can surface their own errors. - maybe_auto_index(cwd=None, project=None, branch=None): opt-in via CODE_GRAPH_AUTO_INDEX env var (off by default — indexing a large repo can take minutes and surprising the user on first call is bad UX). Detects current branch via `git rev-parse`, falls back to `_default`. Per-(project, branch) idempotency via a module-level set; second call for the same key is a no-op. - _truthy helper accepts 1/true/yes/on (case insensitive). api/mcp/server.py - main() now runs ensure_falkordb() and maybe_auto_index() before app.run(). Module-level import behaviour unchanged (tests that `import api.mcp.server` don't trigger any I/O). tests/mcp/test_auto_init.py (9 tests) - ensure_falkordb: no-op when reachable, runs cgraph when not, skips Docker for remote hosts, handles missing CLI binary. - maybe_auto_index: skipped when env unset, indexes when opt-in, idempotent across calls for same key, distinct branches each get one auto-index, _truthy semantics. All mocks — no Docker, no real FalkorDB writes — so the tests run in <2s without external dependencies. Out of scope per ticket: watch mode / re-indexing on FS change, auto-pulling Docker image (cgraph ensure-db handles that), cross- session state. Closes #660. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/mcp/auto_init.py | 172 ++++++++++++++++++++++++++++++++++++ api/mcp/server.py | 10 ++- tests/mcp/test_auto_init.py | 168 +++++++++++++++++++++++++++++++++++ 3 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 api/mcp/auto_init.py create mode 100644 tests/mcp/test_auto_init.py diff --git a/api/mcp/auto_init.py b/api/mcp/auto_init.py new file mode 100644 index 00000000..86ac913d --- /dev/null +++ b/api/mcp/auto_init.py @@ -0,0 +1,172 @@ +"""Zero-config startup helpers for the MCP server (T12). + +Two automation behaviours: + +1. :func:`ensure_falkordb` — at server boot, ping FalkorDB; if it's + unreachable on a localhost host, run ``cgraph ensure-db`` to spin up + the existing Docker container. Reuses ``api.cli.ensure_db`` rather + than duplicating Docker logic. + +2. :func:`maybe_auto_index` — when ``CODE_GRAPH_AUTO_INDEX=true`` is set + (opt-in, off by default), index the current working directory into a + per-branch graph so the agent doesn't have to call ``index_repo`` + first. Idempotent within a single process — the second call for the + same ``(project, branch)`` is a no-op. + +Both are deliberately conservative: ensure-db only acts on localhost +hosts, and auto-index requires explicit opt-in because indexing a +large repo can take minutes and surprising the user with that on +first tool call is bad UX. +""" + +from __future__ import annotations + +import logging +import os +import socket +import subprocess +from pathlib import Path +from typing import Iterable, Optional + + +logger = logging.getLogger(__name__) + + +_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1"} +_AUTO_INDEXED: set[tuple[str, str]] = set() + + +# --------------------------------------------------------------------------- +# ensure_falkordb +# --------------------------------------------------------------------------- + + +def _falkordb_reachable(host: str, port: int, timeout: float = 1.0) -> bool: + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except OSError: + return False + + +def ensure_falkordb() -> dict: + """Make sure FalkorDB is reachable; bootstrap Docker if not. + + Returns a small status dict so the caller can log it. Never raises — + the goal is to start the MCP server even if the bootstrap fails; + individual tools will then surface their own errors. + """ + host = os.getenv("FALKORDB_HOST", "localhost") + try: + port = int(os.getenv("FALKORDB_PORT", "6379")) + except ValueError: + return {"status": "error", "message": "invalid FALKORDB_PORT"} + + if _falkordb_reachable(host, port): + return {"status": "ok", "host": host, "port": port, "action": "none"} + + if host not in _LOCAL_HOSTS: + return { + "status": "error", + "host": host, + "port": port, + "message": "FalkorDB unreachable; auto-start only supports localhost", + } + + logger.info("FalkorDB unreachable on %s:%s — running `cgraph ensure-db`", host, port) + try: + # Subprocess so the CLI's stdout (which prints JSON) doesn't pollute + # the MCP server's own stdio transport. + result = subprocess.run( + ["cgraph", "ensure-db"], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return {"status": "error", "message": "cgraph CLI not on PATH"} + + return { + "status": "ok" if result.returncode == 0 else "error", + "host": host, + "port": port, + "action": "started", + "stdout": result.stdout.strip(), + "stderr": result.stderr.strip(), + } + + +# --------------------------------------------------------------------------- +# maybe_auto_index +# --------------------------------------------------------------------------- + + +def _truthy(val: Optional[str]) -> bool: + return (val or "").strip().lower() in {"1", "true", "yes", "on"} + + +def _detect_branch(cwd: Path) -> str: + """Best-effort current-branch detection. Falls back to ``_default``.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=str(cwd), + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + except FileNotFoundError: + pass + return "_default" + + +def maybe_auto_index( + cwd: Optional[Path] = None, + *, + project: Optional[str] = None, + branch: Optional[str] = None, +) -> dict: + """If opt-in env var is set, index ``cwd`` into the per-branch graph. + + Caches "already auto-indexed this session" per ``(project, branch)`` + in the module-level :data:`_AUTO_INDEXED` set so subsequent calls + are no-ops. + """ + if not _truthy(os.getenv("CODE_GRAPH_AUTO_INDEX")): + return {"status": "skipped", "reason": "CODE_GRAPH_AUTO_INDEX not set"} + + cwd_path = (cwd or Path.cwd()).resolve() + project_name = project or cwd_path.name + branch_name = branch or _detect_branch(cwd_path) + + key = (project_name, branch_name) + if key in _AUTO_INDEXED: + return {"status": "skipped", "reason": "already auto-indexed", "key": key} + + # Local imports so the MCP server can import this module without paying + # the analyzer-stack import cost at module load. + from api.analyzers.source_analyzer import SourceAnalyzer + from api.graph import Graph + + logger.info("Auto-indexing %s @ %s into code:%s:%s", cwd_path, branch_name, project_name, branch_name) + graph = Graph(project_name, branch=branch_name) + SourceAnalyzer().analyze_local_folder(str(cwd_path), graph) + + _AUTO_INDEXED.add(key) + return { + "status": "indexed", + "project": project_name, + "branch": branch_name, + "path": str(cwd_path), + } + + +def reset_auto_index_cache(keys: Optional[Iterable[tuple[str, str]]] = None) -> None: + """Drop the auto-index session cache. Tests only.""" + if keys is None: + _AUTO_INDEXED.clear() + else: + for k in keys: + _AUTO_INDEXED.discard(k) diff --git a/api/mcp/server.py b/api/mcp/server.py index 63ce5cfa..7be41464 100644 --- a/api/mcp/server.py +++ b/api/mcp/server.py @@ -22,8 +22,16 @@ def main() -> None: """Run the MCP server over stdio. - Console-script entry point for ``cgraph-mcp``. + Console-script entry point for ``cgraph-mcp``. Runs the T12 + auto-init helpers first so a freshly-cloned user gets a working + FalkorDB without manual `cgraph ensure-db`, and (opt-in via + ``CODE_GRAPH_AUTO_INDEX``) an indexed CWD without manual + `index_repo`. """ + from .auto_init import ensure_falkordb, maybe_auto_index + + ensure_falkordb() + maybe_auto_index() app.run(transport="stdio") diff --git a/tests/mcp/test_auto_init.py b/tests/mcp/test_auto_init.py new file mode 100644 index 00000000..f7db2f5f --- /dev/null +++ b/tests/mcp/test_auto_init.py @@ -0,0 +1,168 @@ +"""T12 — auto_init tests (mocked subprocess / graph).""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# ensure_falkordb +# --------------------------------------------------------------------------- + + +def test_ensure_falkordb_no_action_when_reachable(monkeypatch): + from api.mcp import auto_init + + monkeypatch.setenv("FALKORDB_HOST", "localhost") + monkeypatch.setenv("FALKORDB_PORT", "6379") + + with patch.object(auto_init, "_falkordb_reachable", return_value=True), \ + patch("api.mcp.auto_init.subprocess.run") as mock_run: + status = auto_init.ensure_falkordb() + + assert status["status"] == "ok" + assert status["action"] == "none" + mock_run.assert_not_called() + + +def test_ensure_falkordb_runs_cgraph_when_unreachable(monkeypatch): + from api.mcp import auto_init + + monkeypatch.setenv("FALKORDB_HOST", "localhost") + monkeypatch.setenv("FALKORDB_PORT", "6379") + + fake_result = MagicMock(returncode=0, stdout="ok", stderr="") + with patch.object(auto_init, "_falkordb_reachable", return_value=False), \ + patch("api.mcp.auto_init.subprocess.run", return_value=fake_result) as mock_run: + status = auto_init.ensure_falkordb() + + assert status["status"] == "ok" + assert status["action"] == "started" + mock_run.assert_called_once() + args = mock_run.call_args.args[0] + assert args == ["cgraph", "ensure-db"] + + +def test_ensure_falkordb_skips_docker_for_remote_host(monkeypatch): + """Auto-start is localhost-only by design.""" + from api.mcp import auto_init + + monkeypatch.setenv("FALKORDB_HOST", "graph.example.com") + monkeypatch.setenv("FALKORDB_PORT", "6379") + + with patch.object(auto_init, "_falkordb_reachable", return_value=False), \ + patch("api.mcp.auto_init.subprocess.run") as mock_run: + status = auto_init.ensure_falkordb() + + assert status["status"] == "error" + assert "localhost" in status["message"] + mock_run.assert_not_called() + + +def test_ensure_falkordb_handles_missing_cli(monkeypatch): + from api.mcp import auto_init + + monkeypatch.setenv("FALKORDB_HOST", "localhost") + monkeypatch.setenv("FALKORDB_PORT", "6379") + + with patch.object(auto_init, "_falkordb_reachable", return_value=False), \ + patch("api.mcp.auto_init.subprocess.run", side_effect=FileNotFoundError): + status = auto_init.ensure_falkordb() + + assert status["status"] == "error" + assert "PATH" in status["message"] + + +# --------------------------------------------------------------------------- +# maybe_auto_index +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def _reset_cache(): + from api.mcp.auto_init import reset_auto_index_cache + + reset_auto_index_cache() + yield + reset_auto_index_cache() + + +def test_maybe_auto_index_skipped_when_env_unset(monkeypatch, tmp_path): + from api.mcp import auto_init + + monkeypatch.delenv("CODE_GRAPH_AUTO_INDEX", raising=False) + + with patch.object(auto_init, "SourceAnalyzer", None, create=True): + status = auto_init.maybe_auto_index(cwd=tmp_path) + + assert status["status"] == "skipped" + assert "CODE_GRAPH_AUTO_INDEX" in status["reason"] + + +def test_maybe_auto_index_indexes_when_opt_in(monkeypatch, tmp_path): + from api.mcp import auto_init + + monkeypatch.setenv("CODE_GRAPH_AUTO_INDEX", "true") + + fake_analyzer_instance = MagicMock() + fake_graph_instance = MagicMock() + with patch("api.analyzers.source_analyzer.SourceAnalyzer", return_value=fake_analyzer_instance), \ + patch("api.graph.Graph", return_value=fake_graph_instance), \ + patch.object(auto_init, "_detect_branch", return_value="main"): + status = auto_init.maybe_auto_index(cwd=tmp_path, project="myproj") + + assert status["status"] == "indexed" + assert status["project"] == "myproj" + assert status["branch"] == "main" + fake_analyzer_instance.analyze_local_folder.assert_called_once() + + +def test_maybe_auto_index_idempotent(monkeypatch, tmp_path): + """Second call for the same (project, branch) is a no-op.""" + from api.mcp import auto_init + + monkeypatch.setenv("CODE_GRAPH_AUTO_INDEX", "1") + + fake_analyzer = MagicMock() + with patch("api.analyzers.source_analyzer.SourceAnalyzer", return_value=fake_analyzer), \ + patch("api.graph.Graph", return_value=MagicMock()), \ + patch.object(auto_init, "_detect_branch", return_value="main"): + first = auto_init.maybe_auto_index(cwd=tmp_path, project="myproj") + second = auto_init.maybe_auto_index(cwd=tmp_path, project="myproj") + + assert first["status"] == "indexed" + assert second["status"] == "skipped" + assert "already" in second["reason"] + # Critical: the analyzer was invoked exactly once. + assert fake_analyzer.analyze_local_folder.call_count == 1 + + +def test_maybe_auto_index_per_branch(monkeypatch, tmp_path): + """Different branches under the same project each get one auto-index.""" + from api.mcp import auto_init + + monkeypatch.setenv("CODE_GRAPH_AUTO_INDEX", "yes") + + fake_analyzer = MagicMock() + with patch("api.analyzers.source_analyzer.SourceAnalyzer", return_value=fake_analyzer), \ + patch("api.graph.Graph", return_value=MagicMock()): + a = auto_init.maybe_auto_index(cwd=tmp_path, project="p", branch="main") + b = auto_init.maybe_auto_index(cwd=tmp_path, project="p", branch="feature-x") + c = auto_init.maybe_auto_index(cwd=tmp_path, project="p", branch="main") + + assert a["status"] == "indexed" + assert b["status"] == "indexed" + assert c["status"] == "skipped" + assert fake_analyzer.analyze_local_folder.call_count == 2 + + +def test_truthy_helper(): + from api.mcp.auto_init import _truthy + + for v in ("1", "true", "TRUE", "yes", "YES", "on"): + assert _truthy(v) + for v in ("", "0", "false", "no", "off", None): + assert not _truthy(v) From 2a71e7fc3ceef1e16c09a7fbce4b798daa239e3b Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:31:02 +0300 Subject: [PATCH 34/63] MCP-T13 + T14: cgraph init-agent + Docker mode switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T13 — agent guidance bundle: - Add `api/mcp/templates/` shipped as package data (claude_mcp_section.md, cursorrules.template) with the canonical 8-tool MCP guidance. - Add `cgraph init-agent [--force]` Typer command that drops CLAUDE.md and .cursorrules into CWD. - Update AGENTS.md with the MCP tool table and env-var reference. T14 — packaging / image dual-mode: - `start.sh` dispatches on `CGRAPH_MODE` env var (web|mcp). Default (web) preserves the existing FastAPI behaviour; mcp execs cgraph-mcp. - `docker-compose.yml` gains an opt-in `code-graph-mcp` service under the `mcp` profile for stdio attach. - README quickstart section for both `claude mcp add-json` registration and Docker compose profile usage. Tests: 4 new (CliRunner against tmp_path); MCP suite green at 61 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 23 ++++++++ README.md | 58 ++++++++++++++++++++ api/cli.py | 42 +++++++++++++++ api/mcp/templates/claude_mcp_section.md | 37 +++++++++++++ api/mcp/templates/cursorrules.template | 32 +++++++++++ docker-compose.yml | 19 ++++++- pyproject.toml | 3 ++ start.sh | 18 +++++-- tests/mcp/test_init_agent.py | 72 +++++++++++++++++++++++++ 9 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 api/mcp/templates/claude_mcp_section.md create mode 100644 api/mcp/templates/cursorrules.template create mode 100644 tests/mcp/test_init_agent.py diff --git a/AGENTS.md b/AGENTS.md index 1b2679b9..608680c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,3 +154,26 @@ cgraph info [--repo ] # Repo stats + metadata ``` `--repo` defaults to the current directory name. Claude Code skill in `skills/code-graph/`. + +## MCP server (for agents) + +`cgraph-mcp` exposes the code graph over MCP stdio. Eight tools: +`index_repo`, `search_code`, `get_callers`, `get_callees`, +`get_dependencies`, `impact_analysis`, `find_path`, `ask`. + +Drop the canonical agent guidance into any repo: + +```bash +cgraph init-agent # writes CLAUDE.md + .cursorrules +cgraph init-agent --force # overwrite existing files +``` + +See `api/mcp/templates/claude_mcp_section.md` for the full tool table +and rules of thumb (start with `search_code`; prefer structural tools +over `ask`; run `impact_analysis` before refactoring). + +Environment: + +- `CODE_GRAPH_AUTO_INDEX=true` — auto-index CWD on MCP startup. +- `CGRAPH_MODE=mcp` — run `cgraph-mcp` instead of the FastAPI web + server when using the Docker image. diff --git a/README.md b/README.md index e2e54bb0..417209ec 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,48 @@ npx skills add FalkorDB/code-graph Then ask Claude things like *"what functions call analyze_sources?"* or *"find the dependency chain between parse_config and send_request"* — it will handle the indexing and querying automatically. +### MCP server (`cgraph-mcp`) + +For agents that speak the [Model Context Protocol](https://modelcontextprotocol.io) +(Claude Code, Cursor, Cline, …), code-graph ships a stdio MCP server +that exposes the knowledge graph as 8 first-class tools: `index_repo`, +`search_code`, `get_callers`, `get_callees`, `get_dependencies`, +`impact_analysis`, `find_path`, and `ask` (NL→Cypher via GraphRAG). + +Quickstart — Claude Code: + +```bash +# 1. Install (in any venv with the cgraph package on PATH) +pip install code-graph # or: uv pip install code-graph + +# 2. Register with Claude Code +claude mcp add-json code-graph '{ + "command": "cgraph-mcp", + "env": { + "FALKORDB_HOST": "localhost", + "FALKORDB_PORT": "6379", + "CODE_GRAPH_AUTO_INDEX": "true" + } +}' + +# 3. Drop agent guidance into your repo +cd /path/to/your/repo +cgraph init-agent # writes CLAUDE.md and .cursorrules +``` + +Quickstart — Docker Compose: + +```bash +docker compose up -d falkordb # start the DB +docker compose --profile mcp run --rm -i code-graph-mcp # attach via stdio +``` + +The MCP server auto-bootstraps FalkorDB if it's missing on localhost +(via `cgraph ensure-db`). When `CODE_GRAPH_AUTO_INDEX=true` is set, +the current working directory is indexed automatically on start. + +**Transport:** Phase 1 is stdio only. HTTP/SSE is deferred. + ## Running with Docker ### Using Docker Compose @@ -232,11 +274,19 @@ docker compose up --build This starts FalkorDB and the CodeGraph app together. The checked-in compose file sets `CODE_GRAPH_PUBLIC=1` for the app service. +To run the **MCP stdio server** instead of the web app from the same +image, set `CGRAPH_MODE=mcp` and use the `mcp` profile: + +```bash +docker compose --profile mcp run --rm -i code-graph-mcp +``` + ### Using Docker directly ```bash docker build -t code-graph . +# Web mode (default) docker run -p 5000:5000 \ -e FALKORDB_HOST=host.docker.internal \ -e FALKORDB_PORT=6379 \ @@ -244,6 +294,14 @@ docker run -p 5000:5000 \ -e GEMINI_API_KEY= \ -e SECRET_TOKEN= \ code-graph + +# MCP stdio mode (same image) +docker run --rm -i \ + -e CGRAPH_MODE=mcp \ + -e FALKORDB_HOST=host.docker.internal \ + -e FALKORDB_PORT=6379 \ + -e MODEL_NAME=gemini/gemini-flash-lite-latest \ + code-graph ``` ## Creating a Code Graph diff --git a/api/cli.py b/api/cli.py index bf6d9d46..bf9ed4d6 100644 --- a/api/cli.py +++ b/api/cli.py @@ -413,5 +413,47 @@ def info( _json_out({"repo": name, "branch": branch, **stats, "metadata": metadata}) +# ── init-agent ───────────────────────────────────────────────────────── + + +_TEMPLATES_DIR = Path(__file__).parent / "mcp" / "templates" + + +@app.command("init-agent") +def init_agent( + force: bool = typer.Option( + False, "--force", "-f", help="Overwrite existing CLAUDE.md / .cursorrules." + ), +) -> None: + """Drop AI-agent guidance files (CLAUDE.md, .cursorrules) into CWD. + + Copies the canonical code-graph MCP guidance bundled with this + package so any repo can announce the tools to Cursor and Claude + Code with one command. + """ + targets = { + "CLAUDE.md": _TEMPLATES_DIR / "claude_mcp_section.md", + ".cursorrules": _TEMPLATES_DIR / "cursorrules.template", + } + + cwd = Path.cwd() + if not force: + existing = [name for name in targets if (cwd / name).exists()] + if existing: + _json_error( + f"Refusing to overwrite existing files: {', '.join(existing)}. " + "Re-run with --force to clobber." + ) + + written: List[str] = [] + for name, template in targets.items(): + dest = cwd / name + dest.write_text(template.read_text(encoding="utf-8"), encoding="utf-8") + written.append(str(dest)) + _stderr(f"Wrote {dest}") + + _json_out({"status": "ok", "written": written, "force": force}) + + if __name__ == "__main__": app() diff --git a/api/mcp/templates/claude_mcp_section.md b/api/mcp/templates/claude_mcp_section.md new file mode 100644 index 00000000..ece08f45 --- /dev/null +++ b/api/mcp/templates/claude_mcp_section.md @@ -0,0 +1,37 @@ +# code-graph MCP server — agent guidance + +This repo is indexed into a FalkorDB **code knowledge graph** exposed +to you over MCP as `code-graph`. Use it instead of grepping when you +need to understand how symbols connect. + +## When to call each tool + +| Tool | Call this when… | Example | +|---|---|---| +| `index_repo(path)` | **First** thing in a new repo; or after large changes outside your edits. | `index_repo(path=".")` | +| `search_code(prefix, project)` | You know part of a symbol name and need its id. | `search_code(prefix="processPay", project="myrepo")` | +| `get_callers(symbol_id, project)` | "Who calls this?" — refactoring a function, tracking down a regression. | `get_callers(symbol_id=42, project="myrepo")` | +| `get_callees(symbol_id, project)` | "What does this call?" — understanding a function before editing it. | `get_callees(symbol_id=42, project="myrepo")` | +| `get_dependencies(symbol_id, project)` | All edges out of a symbol (CALLS + IMPORTS + DEFINES). | `get_dependencies(symbol_id=42, project="myrepo")` | +| `impact_analysis(symbol_id, project, direction, depth)` | **"What breaks if I change this?"** Transitive upstream callers. | `impact_analysis(symbol_id=42, project="myrepo", direction="IN", depth=3)` | +| `find_path(source_id, dest_id, project)` | Show the call chain between two known symbols. | `find_path(source_id=10, dest_id=42, project="myrepo")` | +| `ask(question, project)` | Open-ended natural-language question. **More expensive — use last.** | `ask(question="why does login fail when MFA is on?", project="myrepo")` | + +## Rules of thumb + +1. **Start with `search_code`** to turn names into ids. Most tools take a `symbol_id`. +2. **Prefer structural tools over `ask`.** `get_callers` is one cheap Cypher + hop; `ask` is two LLM round-trips. Use `ask` for fuzzy/conceptual + questions, not for "who calls X". +3. **`impact_analysis` before refactoring.** Even when you think you know + the answer — the transitive closure often surprises you. +4. **`branch` is optional** but pass it when working on a feature branch + so you query the right per-branch index. + +## Environment + +- `CODE_GRAPH_AUTO_INDEX=true` — auto-index CWD on first tool call (off by + default; opt-in because indexing big repos takes minutes). +- `FALKORDB_HOST` / `FALKORDB_PORT` — defaults to `localhost:6379`. If + unreachable on localhost, the server runs `cgraph ensure-db` to + spin up the official Docker image. diff --git a/api/mcp/templates/cursorrules.template b/api/mcp/templates/cursorrules.template new file mode 100644 index 00000000..aa449eaf --- /dev/null +++ b/api/mcp/templates/cursorrules.template @@ -0,0 +1,32 @@ +# Cursor rules — code-graph MCP + +This project is indexed into a FalkorDB code knowledge graph via the +`code-graph` MCP server. Use it instead of grepping when you need to +understand how symbols connect. + +## Tool selection + +- Symbol lookup by name: use `code-graph.search_code` (gives you the + numeric id every other tool needs). +- "Who calls X?": `code-graph.get_callers`. +- "What does X call?": `code-graph.get_callees`. +- All outgoing edges (CALLS + IMPORTS + DEFINES): `code-graph.get_dependencies`. +- Refactoring impact ("what breaks if I change X"): + `code-graph.impact_analysis` with `direction="IN"`. +- Call chain between two specific symbols: `code-graph.find_path`. +- Natural-language question over the graph (expensive — last resort): + `code-graph.ask`. + +## Rules + +- Always `search_code` first to resolve names to ids. +- Prefer structural tools (`get_callers`, `find_path`, `impact_analysis`) + over `ask` for "who/what/where" questions. +- Run `impact_analysis(direction="IN", depth=3)` before any non-trivial + refactor. +- Pass `branch` when on a feature branch. + +## First run + +If the repo isn't indexed yet, call `index_repo(path=".")` once. After +that, navigate via the structural tools. diff --git a/docker-compose.yml b/docker-compose.yml index e6de30d2..dd2e9f30 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,4 +22,21 @@ services: - FALKORDB_PORT=6379 - OPENAI_API_KEY=${OPENAI_API_KEY:-} - SECRET_TOKEN=${SECRET_TOKEN:-} - - CODE_GRAPH_PUBLIC=1 \ No newline at end of file + - CODE_GRAPH_PUBLIC=1 + + # MCP stdio server — opt-in. Bring up with: + # docker compose run --rm -i code-graph-mcp + # then point Claude Code / Cursor at the running container's stdio. + code-graph-mcp: + build: . + depends_on: + - falkordb + profiles: ["mcp"] + stdin_open: true + tty: false + environment: + - CGRAPH_MODE=mcp + - FALKORDB_HOST=falkordb + - FALKORDB_PORT=6379 + - MODEL_NAME=${MODEL_NAME:-gemini/gemini-flash-lite-latest} + - CODE_GRAPH_AUTO_INDEX=${CODE_GRAPH_AUTO_INDEX:-} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ab0c176e..c18f5629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["."] +[tool.setuptools.package-data] +"api.mcp" = ["templates/*"] + [dependency-groups] dev = [ "pytest>=9.0.2", diff --git a/start.sh b/start.sh index b01ffed7..05394b8c 100755 --- a/start.sh +++ b/start.sh @@ -4,6 +4,7 @@ set -e # Set default values if not set FALKORDB_HOST="${FALKORDB_HOST:-localhost}" FALKORDB_PORT="${FALKORDB_PORT:-6379}" +CGRAPH_MODE="${CGRAPH_MODE:-web}" # Start FalkorDB Redis server in background only if using a local address (not an external instance) if [ "${FALKORDB_HOST}" = "localhost" ] || [[ "${FALKORDB_HOST}" =~ ^127\.0\.0\.[0-9]+$ ]]; then @@ -12,7 +13,7 @@ fi # Wait until FalkorDB is ready FALKORDB_WAIT_TIMEOUT="${FALKORDB_WAIT_TIMEOUT:-30}" -echo "Waiting for FalkorDB to start on $FALKORDB_HOST:$FALKORDB_PORT (timeout: ${FALKORDB_WAIT_TIMEOUT}s)..." +echo "Waiting for FalkorDB to start on $FALKORDB_HOST:$FALKORDB_PORT (timeout: ${FALKORDB_WAIT_TIMEOUT}s)..." >&2 SECONDS=0 while ! nc -z "$FALKORDB_HOST" "$FALKORDB_PORT"; do @@ -23,7 +24,16 @@ while ! nc -z "$FALKORDB_HOST" "$FALKORDB_PORT"; do sleep 0.5 done -echo "FalkorDB is up - launching server..." +echo "FalkorDB is up — launching ${CGRAPH_MODE} mode..." >&2 -# Start the backend -exec uvicorn api.index:app --host "${HOST:-0.0.0.0}" --port "${PORT:-5000}" ${APP_RELOAD:+--reload} +# Dispatch on CGRAPH_MODE. Default ("web") preserves the original +# behaviour. "mcp" runs the stdio MCP server so the same image can be +# attached to Claude Code / Cursor without rebuilding. +case "${CGRAPH_MODE}" in + mcp) + exec cgraph-mcp + ;; + web|*) + exec uvicorn api.index:app --host "${HOST:-0.0.0.0}" --port "${PORT:-5000}" ${APP_RELOAD:+--reload} + ;; +esac diff --git a/tests/mcp/test_init_agent.py b/tests/mcp/test_init_agent.py new file mode 100644 index 00000000..4a14ba71 --- /dev/null +++ b/tests/mcp/test_init_agent.py @@ -0,0 +1,72 @@ +"""Tests for `cgraph init-agent` (T13). + +Verifies the CLI drops the MCP guidance templates into CWD and +respects `--force`. +""" + +import os + +import pytest +from typer.testing import CliRunner + +from api.cli import app + + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +def _cd(monkeypatch: pytest.MonkeyPatch, path) -> None: + monkeypatch.chdir(path) + + +def test_init_agent_writes_both_files(runner, tmp_path, monkeypatch): + _cd(monkeypatch, tmp_path) + + result = runner.invoke(app, ["init-agent"]) + + assert result.exit_code == 0, result.output + claude = tmp_path / "CLAUDE.md" + cursor = tmp_path / ".cursorrules" + assert claude.exists() + assert cursor.exists() + # Templates should mention at least one core MCP tool. + body = claude.read_text() + cursor.read_text() + assert "search_code" in body + assert "code-graph" in body.lower() + + +def test_init_agent_refuses_overwrite_without_force(runner, tmp_path, monkeypatch): + _cd(monkeypatch, tmp_path) + (tmp_path / "CLAUDE.md").write_text("preexisting\n") + + result = runner.invoke(app, ["init-agent"]) + + assert result.exit_code != 0 + # Existing file untouched. + assert (tmp_path / "CLAUDE.md").read_text() == "preexisting\n" + # And we didn't write the other one either. + assert not (tmp_path / ".cursorrules").exists() + + +def test_init_agent_force_overwrites(runner, tmp_path, monkeypatch): + _cd(monkeypatch, tmp_path) + (tmp_path / "CLAUDE.md").write_text("preexisting\n") + (tmp_path / ".cursorrules").write_text("old rules\n") + + result = runner.invoke(app, ["init-agent", "--force"]) + + assert result.exit_code == 0, result.output + assert "preexisting" not in (tmp_path / "CLAUDE.md").read_text() + assert "old rules" not in (tmp_path / ".cursorrules").read_text() + assert "search_code" in (tmp_path / "CLAUDE.md").read_text() + + +def test_init_agent_templates_ship_with_package(): + """Smoke check: the bundled template files exist on disk where the + CLI expects them. Guards against pyproject.toml drift.""" + from api.cli import _TEMPLATES_DIR + + assert (_TEMPLATES_DIR / "claude_mcp_section.md").is_file() + assert (_TEMPLATES_DIR / "cursorrules.template").is_file() From a18854be3528bf352e68ee250496813612a68edb Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:39:27 +0300 Subject: [PATCH 35/63] MCP smoke harness + template fixes from end-to-end run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added scripts/mcp_smoke.py — drives cgraph-mcp over real stdio (mcp SDK ClientSession + StdioServerParameters) and exercises tool listing, index_repo, search_code, get_callers, impact_analysis. Findings folded back into claude_mcp_section.md: - index_repo takes path_or_url (not path) and derives project name from the folder/URL — agents must read it back from the response. - Collection-returning tools land their array in structuredContent.result, not under a {results: [...]} wrapper. Smoke result on api/ subgraph: 8 tools listed, 6324 nodes / 6228 edges indexed, all calls returned expected payloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/mcp/templates/claude_mcp_section.md | 7 +- scripts/mcp_smoke.py | 180 ++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 scripts/mcp_smoke.py diff --git a/api/mcp/templates/claude_mcp_section.md b/api/mcp/templates/claude_mcp_section.md index ece08f45..adf8b009 100644 --- a/api/mcp/templates/claude_mcp_section.md +++ b/api/mcp/templates/claude_mcp_section.md @@ -8,7 +8,7 @@ need to understand how symbols connect. | Tool | Call this when… | Example | |---|---|---| -| `index_repo(path)` | **First** thing in a new repo; or after large changes outside your edits. | `index_repo(path=".")` | +| `index_repo(path_or_url, branch?)` | **First** thing in a new repo; or after large changes outside your edits. Project name is **derived from the folder or repo URL** — read it back from the response. | `index_repo(path_or_url=".")` | | `search_code(prefix, project)` | You know part of a symbol name and need its id. | `search_code(prefix="processPay", project="myrepo")` | | `get_callers(symbol_id, project)` | "Who calls this?" — refactoring a function, tracking down a regression. | `get_callers(symbol_id=42, project="myrepo")` | | `get_callees(symbol_id, project)` | "What does this call?" — understanding a function before editing it. | `get_callees(symbol_id=42, project="myrepo")` | @@ -27,6 +27,11 @@ need to understand how symbols connect. the answer — the transitive closure often surprises you. 4. **`branch` is optional** but pass it when working on a feature branch so you query the right per-branch index. +5. **Response shape.** Tools that return collections (`search_code`, + `get_callers`, `get_callees`, `get_dependencies`, `find_path`, + `impact_analysis`) put the array in `structuredContent.result` per + the MCP spec. The text content is the same JSON for convenience. + `index_repo` and `ask` return a single object. ## Environment diff --git a/scripts/mcp_smoke.py b/scripts/mcp_smoke.py new file mode 100644 index 00000000..38e25d44 --- /dev/null +++ b/scripts/mcp_smoke.py @@ -0,0 +1,180 @@ +"""End-to-end MCP smoke test. + +Spawns `cgraph-mcp` over stdio, lists tools, indexes the +code-graph repo itself, and exercises `search_code`, +`get_callers`, and `impact_analysis`. Prints a compact pass/fail line per +tool. +""" + +import asyncio +import json +import os +import sys +from pathlib import Path + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + + +REPO_ROOT = Path(__file__).resolve().parent.parent +INDEX_PATH = REPO_ROOT / "api" +PROJECT_NAME = "code-graph-mcp-smoke" +BRANCH = "smoke" + + +def _pretty(result): + """Pull the first text payload out of a CallToolResult.""" + for chunk in result.content: + if hasattr(chunk, "text"): + try: + return json.loads(chunk.text) + except Exception: + return chunk.text + # No text chunks — show structured content if MCP put the payload there. + if hasattr(result, "structuredContent") and result.structuredContent is not None: + return result.structuredContent + return None + + +async def main() -> int: + env = { + **os.environ, + "FALKORDB_HOST": os.environ.get("FALKORDB_HOST", "127.0.0.1"), + "FALKORDB_PORT": os.environ.get("FALKORDB_PORT", "6390"), + } + + params = StdioServerParameters( + command="cgraph-mcp", + args=[], + env=env, + ) + + fails = 0 + + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + + tools = await session.list_tools() + tool_names = sorted(t.name for t in tools.tools) + print(f"[tools] {len(tool_names)}: {tool_names}") + expected = { + "index_repo", + "search_code", + "get_callers", + "get_callees", + "get_dependencies", + "impact_analysis", + "find_path", + "ask", + } + missing = expected - set(tool_names) + if missing: + print(f"[FAIL] missing tools: {missing}") + fails += 1 + + print(f"[index_repo] indexing {INDEX_PATH} ...") + idx = await session.call_tool( + "index_repo", + { + "path_or_url": str(INDEX_PATH), + "branch": BRANCH, + "ignore": [".venv", "node_modules", ".git", "app/dist"], + }, + ) + idx_payload = _pretty(idx) + print(f"[index_repo] -> {json.dumps(idx_payload)[:200]}") + if not isinstance(idx_payload, dict) or idx_payload.get("error"): + print("[FAIL] index_repo did not return ok payload") + fails += 1 + return 1 + project_name = idx_payload["project_name"] + branch_name = idx_payload["branch"] + print(f"[index_repo] graph={idx_payload['graph_name']} project={project_name}") + + print("[search_code] prefix='index_repo'") + sr = await session.call_tool( + "search_code", + {"prefix": "index_repo", "project": project_name, "branch": branch_name}, + ) + sr_payload = _pretty(sr) + print(f"[search_code] -> {json.dumps(sr_payload)[:300]}") + if isinstance(sr_payload, list): + hits = sr_payload + elif isinstance(sr_payload, dict) and "results" in sr_payload: + hits = sr_payload["results"] + elif isinstance(sr_payload, dict) and "id" in sr_payload: + hits = [sr_payload] + else: + hits = [] + if not hits: + print("[FAIL] search_code returned no hits for index_repo") + fails += 1 + first_id = None + else: + first_id = hits[0].get("id") + print(f"[search_code] picked id={first_id} name={hits[0].get('name')}") + + if first_id is not None: + print(f"[get_callers] id={first_id}") + gc = await session.call_tool( + "get_callers", + { + "symbol_id": first_id, + "project": project_name, + "branch": branch_name, + }, + ) + gc_payload = _pretty(gc) + # Some MCP servers return list payloads in structuredContent only. + gc_struct = getattr(gc, "structuredContent", None) + print(f"[get_callers] -> {json.dumps(gc_payload)[:300]} struct={json.dumps(gc_struct)[:200]}") + # Acceptable shapes: list of caller dicts, or {"callers": [...]}. + callers = None + if isinstance(gc_payload, list): + callers = gc_payload + elif isinstance(gc_payload, dict) and "callers" in gc_payload: + callers = gc_payload["callers"] + elif isinstance(gc_struct, dict) and "result" in gc_struct: + callers = gc_struct["result"] + if callers is None: + print("[FAIL] get_callers returned no recognizable payload") + fails += 1 + else: + print(f"[get_callers] {len(callers)} callers") + + print("[impact_analysis] depth=2") + ia = await session.call_tool( + "impact_analysis", + { + "symbol_id": first_id, + "depth": 2, + "project": project_name, + "branch": branch_name, + }, + ) + ia_payload = _pretty(ia) + ia_struct = getattr(ia, "structuredContent", None) + print(f"[impact_analysis] -> {json.dumps(ia_payload)[:300]} struct={json.dumps(ia_struct)[:200]}") + impacted = None + if isinstance(ia_payload, dict) and "impacted" in ia_payload: + impacted = ia_payload["impacted"] + elif isinstance(ia_struct, dict) and "impacted" in ia_struct: + impacted = ia_struct["impacted"] + elif isinstance(ia_struct, dict) and "result" in ia_struct: + impacted = ia_struct["result"] + if impacted is None: + print("[FAIL] impact_analysis no 'impacted' field") + fails += 1 + else: + print(f"[impact_analysis] {len(impacted)} impacted") + + if fails: + print(f"\n=== {fails} FAILED ===") + return 1 + print("\n=== ALL OK ===") + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main())) From 60e2bd1d9c8afffa16e2e029f16ef1990edd76fd Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:48:01 +0300 Subject: [PATCH 36/63] bench: add MCP-transport sibling of the code_graph track MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second bench track that exercises code-graph through the exact transport real-world agents use (Claude Code, Cursor, …) — JSON-RPC over stdio to a spawned `cgraph-mcp` server — instead of HTTP to the FastAPI service. Files: - bench/agents/code_graph_mcp_adapter.py — sync Python adapter that spawns cgraph-mcp per call via the official MCP Python SDK. Knows the 8-tool MCP surface (search_code, get_callers, get_callees, get_dependencies, impact_analysis, find_path, index_repo, ask). - bench/cli/cg-mcp + cg_mcp.py — bash-callable CLI shim mirroring the existing `cg` shim. mini-swe-agent only does bash, so each "tool" is one CLI invocation. - bench/tools/code_graph_mcp/{tools.yaml,system_preamble.md} — agent config for the MCP track. Mirrors code_graph; same Q2 decision to exclude `ask` (no nested LLM in the benchmarked tool set). - tests/bench/test_cg_mcp_adapter.py — 5 unit + 1 e2e test (FalkorDB-gated AND MCP-server-gated so it skips cleanly until the MCP stack lands on staging). Heavy e2e validated against the api/ subgraph (~6.3k nodes) over real stdio: search_code -> get_callers -> impact_analysis returned expected payloads. Depends on the MCP stack (PRs #675–#683) for cgraph-mcp itself. Lands cleanly once that stack merges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_mcp_adapter.py | 163 ++++++++++++++++ bench/cli/cg-mcp | 5 + bench/cli/cg_mcp.py | 140 ++++++++++++++ bench/tools/code_graph_mcp/system_preamble.md | 72 +++++++ bench/tools/code_graph_mcp/tools.yaml | 39 ++++ tests/bench/__init__.py | 0 tests/bench/test_cg_mcp_adapter.py | 178 ++++++++++++++++++ 7 files changed, 597 insertions(+) create mode 100644 bench/agents/code_graph_mcp_adapter.py create mode 100755 bench/cli/cg-mcp create mode 100644 bench/cli/cg_mcp.py create mode 100644 bench/tools/code_graph_mcp/system_preamble.md create mode 100644 bench/tools/code_graph_mcp/tools.yaml create mode 100644 tests/bench/__init__.py create mode 100644 tests/bench/test_cg_mcp_adapter.py diff --git a/bench/agents/code_graph_mcp_adapter.py b/bench/agents/code_graph_mcp_adapter.py new file mode 100644 index 00000000..9a6347bd --- /dev/null +++ b/bench/agents/code_graph_mcp_adapter.py @@ -0,0 +1,163 @@ +"""MCP-transport adapter to cgraph-mcp for the benchmark. + +Sibling of `code_graph_adapter.py` (HTTP). Where the HTTP adapter talks +to the host FastAPI service over the network, this one spawns the +`cgraph-mcp` stdio MCP server in-process via the official MCP Python +SDK and dispatches tool calls over JSON-RPC. + +This gives us a second, real-world benchmark track that exercises the +exact same transport agents (Claude Code, Cursor, …) will use in +production. Tool names match the 8-tool MCP surface +(`index_repo`, `search_code`, `get_callers`, `get_callees`, +`get_dependencies`, `impact_analysis`, `find_path`, `ask`). + +Each call spawns a fresh server, runs the call, and exits. That's +~0.5-1s overhead per call but keeps the model trivially safe to call +from a bash shim (one process per invocation, no shared state). +A future optimisation could persist the server across calls via a +side-channel daemon, but per-call spawn matches how external agents +actually use MCP servers today. +""" + +from __future__ import annotations + +import asyncio +import json +import os +from typing import Any + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + + +DEFAULT_TIMEOUT_SEC = 60.0 + + +def _env_for_mcp() -> dict[str, str]: + """Build the env for the spawned cgraph-mcp process. + + Pass through everything from the caller but make sure the FalkorDB + coordinates are present — the runner usually sets them to point at + the host FalkorDB container. + """ + env = dict(os.environ) + env.setdefault("FALKORDB_HOST", os.environ.get("FALKORDB_HOST", "127.0.0.1")) + env.setdefault("FALKORDB_PORT", os.environ.get("FALKORDB_PORT", "6379")) + return env + + +def _extract(result: Any) -> Any: + """Normalize a CallToolResult into a JSON-serialisable Python value. + + The MCP spec lets servers put the payload in `structuredContent` + and/or echo it as a JSON text chunk. Our 8 tools do both; agents + have historically preferred the text payload. We mirror that: + return the parsed text chunk when present, otherwise fall back to + structuredContent (unwrapping the spec's `{"result": ...}` wrapper + for collection-returning tools). + """ + for chunk in result.content: + if hasattr(chunk, "text") and chunk.text: + try: + return json.loads(chunk.text) + except json.JSONDecodeError: + return chunk.text + struct = getattr(result, "structuredContent", None) + if isinstance(struct, dict) and set(struct.keys()) == {"result"}: + return struct["result"] + return struct + + +async def _call_tool_async(name: str, arguments: dict[str, Any], timeout: float) -> Any: + params = StdioServerParameters(command="cgraph-mcp", args=[], env=_env_for_mcp()) + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await asyncio.wait_for(session.initialize(), timeout=timeout) + result = await asyncio.wait_for( + session.call_tool(name, arguments), timeout=timeout + ) + payload = _extract(result) + if getattr(result, "isError", False): + return {"error": payload} + return payload + + +def call_tool(name: str, arguments: dict[str, Any], *, timeout: float = DEFAULT_TIMEOUT_SEC) -> Any: + """Sync entry point for the bash shim. One spawn per call.""" + return asyncio.run(_call_tool_async(name, arguments, timeout)) + + +# ── Top-level convenience wrappers ───────────────────────────────────── +# Names map 1:1 onto MCP tool names (and onto bench/tools/code_graph_mcp/ +# tools.yaml entries). Kwargs mirror each tool's MCP arg schema. + + +def index_repo(path_or_url: str, branch: str | None = None, ignore: list[str] | None = None) -> dict[str, Any]: + args: dict[str, Any] = {"path_or_url": path_or_url} + if branch is not None: + args["branch"] = branch + if ignore is not None: + args["ignore"] = ignore + return call_tool("index_repo", args) + + +def search_code(prefix: str, project: str, branch: str | None = None, limit: int = 10) -> Any: + args: dict[str, Any] = {"prefix": prefix, "project": project, "limit": limit} + if branch is not None: + args["branch"] = branch + return call_tool("search_code", args) + + +def _neighbors(tool: str, symbol_id: int, project: str, branch: str | None, limit: int) -> Any: + args: dict[str, Any] = {"symbol_id": symbol_id, "project": project, "limit": limit} + if branch is not None: + args["branch"] = branch + return call_tool(tool, args) + + +def get_callers(symbol_id: int, project: str, branch: str | None = None, limit: int = 50) -> Any: + return _neighbors("get_callers", symbol_id, project, branch, limit) + + +def get_callees(symbol_id: int, project: str, branch: str | None = None, limit: int = 50) -> Any: + return _neighbors("get_callees", symbol_id, project, branch, limit) + + +def get_dependencies(symbol_id: int, project: str, branch: str | None = None, limit: int = 50) -> Any: + return _neighbors("get_dependencies", symbol_id, project, branch, limit) + + +def impact_analysis( + symbol_id: int, + project: str, + branch: str | None = None, + direction: str = "IN", + depth: int = 3, +) -> Any: + args: dict[str, Any] = { + "symbol_id": symbol_id, + "project": project, + "direction": direction, + "depth": depth, + } + if branch is not None: + args["branch"] = branch + return call_tool("impact_analysis", args) + + +def find_path(source_id: int, dest_id: int, project: str, branch: str | None = None) -> Any: + args: dict[str, Any] = { + "source_id": source_id, + "dest_id": dest_id, + "project": project, + } + if branch is not None: + args["branch"] = branch + return call_tool("find_path", args) + + +def ask(question: str, project: str, branch: str | None = None) -> Any: + args: dict[str, Any] = {"question": question, "project": project} + if branch is not None: + args["branch"] = branch + return call_tool("ask", args) diff --git a/bench/cli/cg-mcp b/bench/cli/cg-mcp new file mode 100755 index 00000000..be6c09bb --- /dev/null +++ b/bench/cli/cg-mcp @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Bash-callable entry point for the code-graph MCP CLI. Mirrors `cg` +# but speaks JSON-RPC over stdio to a spawned `cgraph-mcp` server +# instead of HTTP to the FastAPI service. Runner adds bench/cli to PATH. +exec "${BENCH_PYTHON:-python3}" -m bench.cli.cg_mcp "$@" diff --git a/bench/cli/cg_mcp.py b/bench/cli/cg_mcp.py new file mode 100644 index 00000000..95c91390 --- /dev/null +++ b/bench/cli/cg_mcp.py @@ -0,0 +1,140 @@ +"""`cg-mcp` — bash-callable CLI exposing code-graph's 8 MCP tools. + +This is the MCP-transport sibling of `cg`. Where `cg` calls the host +FastAPI service over HTTP, `cg-mcp` spawns the `cgraph-mcp` stdio +server (via the official MCP Python SDK) for every invocation and +dispatches one tool call. + +The MCP track is what external agents (Claude Code, Cursor, …) use +in production; benchmarking through it tells us how the *real-world* +integration behaves under SWE-bench, not just the in-process FastAPI +adapter. + +Subcommands mirror the MCP tool names: + + cg-mcp index_repo --path-or-url . [--branch B] [--ignore PAT ...] + cg-mcp search_code --project P --prefix STR [--branch B] [--limit N] + cg-mcp get_callers --project P --symbol-id ID [--branch B] [--limit N] + cg-mcp get_callees --project P --symbol-id ID [--branch B] [--limit N] + cg-mcp get_dependencies --project P --symbol-id ID [--branch B] [--limit N] + cg-mcp impact_analysis --project P --symbol-id ID [--direction IN|OUT] [--depth N] + cg-mcp find_path --project P --source-id ID --dest-id ID [--branch B] + cg-mcp ask --project P --question "..." [--branch B] + +Output: one JSON document per call on stdout. Errors print to stderr +and exit non-zero. + +Env: FALKORDB_HOST / FALKORDB_PORT are passed through to the spawned +server. Optionally set CGRAPH_MCP_TIMEOUT_SEC to override the +default 60s timeout. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from typing import Any + +from bench.agents import code_graph_mcp_adapter as cgm + + +def _print(obj: Any) -> None: + json.dump(obj, sys.stdout, indent=2, sort_keys=True, default=str) + sys.stdout.write("\n") + + +def _timeout() -> float: + try: + return float(os.getenv("CGRAPH_MCP_TIMEOUT_SEC", "60")) + except ValueError: + return 60.0 + + +def _add_project(p: argparse.ArgumentParser) -> None: + p.add_argument("--project", required=True) + p.add_argument("--branch", default=None) + + +def _add_symbol(p: argparse.ArgumentParser) -> None: + p.add_argument("--symbol-id", type=int, required=True, dest="symbol_id") + p.add_argument("--limit", type=int, default=50) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="cg-mcp", description=__doc__) + sub = parser.add_subparsers(dest="cmd", required=True) + + ir = sub.add_parser("index_repo") + ir.add_argument("--path-or-url", required=True, dest="path_or_url") + ir.add_argument("--branch", default=None) + ir.add_argument("--ignore", nargs="*", default=None) + + sc = sub.add_parser("search_code") + _add_project(sc) + sc.add_argument("--prefix", required=True) + sc.add_argument("--limit", type=int, default=10) + + for name in ("get_callers", "get_callees", "get_dependencies"): + p = sub.add_parser(name) + _add_project(p) + _add_symbol(p) + + ia = sub.add_parser("impact_analysis") + _add_project(ia) + ia.add_argument("--symbol-id", type=int, required=True, dest="symbol_id") + ia.add_argument("--direction", choices=["IN", "OUT"], default="IN") + ia.add_argument("--depth", type=int, default=3) + + fp = sub.add_parser("find_path") + _add_project(fp) + fp.add_argument("--source-id", type=int, required=True, dest="source_id") + fp.add_argument("--dest-id", type=int, required=True, dest="dest_id") + + aq = sub.add_parser("ask") + _add_project(aq) + aq.add_argument("--question", required=True) + + args = parser.parse_args(argv) + timeout = _timeout() + + # Inject timeout for adapter calls. + cgm.DEFAULT_TIMEOUT_SEC = timeout + + try: + if args.cmd == "index_repo": + _print(cgm.index_repo(args.path_or_url, branch=args.branch, ignore=args.ignore)) + elif args.cmd == "search_code": + _print(cgm.search_code(args.prefix, args.project, branch=args.branch, limit=args.limit)) + elif args.cmd == "get_callers": + _print(cgm.get_callers(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + elif args.cmd == "get_callees": + _print(cgm.get_callees(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + elif args.cmd == "get_dependencies": + _print(cgm.get_dependencies(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + elif args.cmd == "impact_analysis": + _print( + cgm.impact_analysis( + args.symbol_id, + args.project, + branch=args.branch, + direction=args.direction, + depth=args.depth, + ) + ) + elif args.cmd == "find_path": + _print(cgm.find_path(args.source_id, args.dest_id, args.project, branch=args.branch)) + elif args.cmd == "ask": + _print(cgm.ask(args.question, args.project, branch=args.branch)) + else: # pragma: no cover — argparse already enforces this + parser.error(f"unknown subcommand: {args.cmd}") + except Exception as e: # noqa: BLE001 — surface everything to the agent + print(f"cg-mcp error: {e}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bench/tools/code_graph_mcp/system_preamble.md b/bench/tools/code_graph_mcp/system_preamble.md new file mode 100644 index 00000000..bf5af4a1 --- /dev/null +++ b/bench/tools/code_graph_mcp/system_preamble.md @@ -0,0 +1,72 @@ +# code-graph (MCP) preamble + +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + +## Code-navigation workflow — use this BEFORE grep/find + +A code-graph **MCP server** (`cgraph-mcp`) is available for this repo. +**Before reading or editing code, locate the relevant symbols through +`cg-mcp` rather than grepping the file tree** — it's faster, returns +precise `{id, file, line}` records, and reveals caller / callee / +impact relationships you would otherwise reconstruct by hand. Fall +back to bash only when `cg-mcp` cannot answer the question. + +`$PROJECT_NAME` and `$BRANCH` are exported for you (do not guess). +The graph is already indexed against the current commit. + +Typical loop: + +1. `cg-mcp search_code --project "$PROJECT_NAME" --prefix ` — + locate a function/class by name. Pick the `id` of the best hit. +2. `cg-mcp get_callers --project "$PROJECT_NAME" --symbol-id ` — + "who calls this?" before refactoring. +3. `cg-mcp impact_analysis --project "$PROJECT_NAME" --symbol-id + --depth 3` — full transitive blast radius. Use this BEFORE + non-trivial edits. +4. Read the implicated file(s) with `sed -n` / `cat`, then edit. + +## Available `cg-mcp` sub-commands + +- `cg-mcp search_code --project P --prefix STR [--limit N]` — + prefix search; returns `[{id, name, label, file, line}, ...]`. +- `cg-mcp get_callers --project P --symbol-id ID [--limit N]` — + incoming CALLS edges (who calls X). +- `cg-mcp get_callees --project P --symbol-id ID [--limit N]` — + outgoing CALLS edges (what X calls). +- `cg-mcp get_dependencies --project P --symbol-id ID [--limit N]` — + all outgoing edges (CALLS + IMPORTS + DEFINES). +- `cg-mcp impact_analysis --project P --symbol-id ID + [--direction IN|OUT] [--depth N]` — + transitive blast radius (default IN, depth 3). +- `cg-mcp find_path --project P --source-id ID --dest-id ID` — + the call chain(s) between two symbols. +- `cg-mcp index_repo --path-or-url PATH [--branch B]` — + (re)index a folder or git URL. Only needed for repos that aren't + pre-indexed. + +You also have the usual Unix tools (`cat`, `grep`/`rg`, `find`, `sed`) +for cases the graph can't answer. + +## Rules of thumb + +1. **Always run `search_code` first** to turn a name into an `id`. +2. **`impact_analysis` before any non-trivial edit.** Even when you + think you know the answer — the transitive closure often surprises + you. +3. **Don't `grep` for callers.** `get_callers` is one cheap Cypher + hop; grep over a large repo costs tens of thousands of tokens. + +## Submission + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. diff --git a/bench/tools/code_graph_mcp/tools.yaml b/bench/tools/code_graph_mcp/tools.yaml new file mode 100644 index 00000000..3b676977 --- /dev/null +++ b/bench/tools/code_graph_mcp/tools.yaml @@ -0,0 +1,39 @@ +# SWE-agent tool bundle: code-graph MCP-transport config. +# +# This is the MCP-transport sibling of bench/tools/code_graph/tools.yaml. +# Same backend graph; different transport. Where `code_graph` calls the +# host FastAPI service over HTTP, `code_graph_mcp` spawns the +# `cgraph-mcp` stdio server for each tool call — the exact transport +# Claude Code / Cursor / Cline use in production. +# +# Tool names mirror the 8 MCP tools registered in api/mcp/tools/ +# (search_code, get_callers, get_callees, get_dependencies, +# impact_analysis, find_path, index_repo, ask). The bash agent calls +# them through the `cg-mcp ...` shim (see bench/cli/cg-mcp). +# +# IMPORTANT: `ask` (GraphRAG) is intentionally NOT in the tool list. +# Including it would double-count tokens (nested LLM agent). Same Q2 +# decision as the HTTP code_graph config — we benchmark the *graph*, +# not GraphRAG. + +extends: ../baseline/tools.yaml + +tools: + - index_repo # (path_or_url, branch?) -> indexing stats + - search_code # (project, prefix) -> [symbol] + - get_callers # (project, symbol_id) -> [caller] + - get_callees # (project, symbol_id) -> [callee] + - get_dependencies # (project, symbol_id) -> [dep] + - impact_analysis # (project, symbol_id, direction, depth) -> [impacted] + - find_path # (project, source_id, dest_id) -> [path] + +backend: + transport: mcp_stdio + command: cgraph-mcp + # Container has cgraph-mcp on PATH via `pip install -e .` against this + # repo. FALKORDB_HOST/PORT are passed through to the spawned MCP + # server, pointing at the same host FalkorDB the HTTP config uses. + env_passthrough: + - FALKORDB_HOST + - FALKORDB_PORT + - MODEL_NAME diff --git a/tests/bench/__init__.py b/tests/bench/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/bench/test_cg_mcp_adapter.py b/tests/bench/test_cg_mcp_adapter.py new file mode 100644 index 00000000..7d6f9274 --- /dev/null +++ b/tests/bench/test_cg_mcp_adapter.py @@ -0,0 +1,178 @@ +"""Tests for the MCP-transport bench adapter (`cg-mcp`). + +Heavy end-to-end test (talks to real cgraph-mcp + FalkorDB) is gated +behind the same `_falkordb_reachable` check as the existing MCP tests. +Light tests run unconditionally and validate the argparse surface and +`_extract` shape handling. +""" + +from __future__ import annotations + +import json +import os +import socket +import subprocess +import sys +from pathlib import Path + +import pytest + +from bench.agents import code_graph_mcp_adapter as cgm +from bench.cli import cg_mcp + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _mcp_server_available() -> bool: + """The benchmark MCP adapter requires the in-repo `cgraph-mcp` server. + + On branches that pre-date the MCP stack (e.g. this branch's base, + `fix-find-symbol-nested-name`), `api.mcp.server` is absent. The + end-to-end test must skip there; it will run on staging once the + MCP stack lands. + """ + try: + import api.mcp.server # noqa: F401 + return True + except ImportError: + return False + + +def _falkordb_reachable() -> bool: + host = os.environ.get("FALKORDB_HOST", "127.0.0.1") + port = int(os.environ.get("FALKORDB_PORT", "6390")) + try: + with socket.create_connection((host, port), timeout=1): + return True + except OSError: + return False + + +# ── light unit tests ────────────────────────────────────────────────── + + +class _FakeChunk: + def __init__(self, text: str) -> None: + self.text = text + + +class _FakeResult: + def __init__(self, content, structured=None, is_error=False): + self.content = content + self.structuredContent = structured + self.isError = is_error + + +def test_extract_prefers_text_chunk_json(): + r = _FakeResult([_FakeChunk('{"id": 7, "name": "foo"}')]) + assert cgm._extract(r) == {"id": 7, "name": "foo"} + + +def test_extract_falls_back_to_structured_result_wrapper(): + r = _FakeResult(content=[], structured={"result": [1, 2, 3]}) + assert cgm._extract(r) == [1, 2, 3] + + +def test_extract_returns_raw_text_when_not_json(): + r = _FakeResult([_FakeChunk("not json at all")]) + assert cgm._extract(r) == "not json at all" + + +def test_cli_rejects_unknown_subcommand(capsys): + with pytest.raises(SystemExit): + cg_mcp.main(["totally_bogus"]) + + +def test_cli_index_repo_parses_ignore_list(monkeypatch): + captured: dict = {} + + def fake_index_repo(path_or_url, branch=None, ignore=None): + captured.update(path_or_url=path_or_url, branch=branch, ignore=ignore) + return {"ok": True, **captured} + + monkeypatch.setattr(cgm, "index_repo", fake_index_repo) + rc = cg_mcp.main( + [ + "index_repo", + "--path-or-url", + "/tmp/x", + "--branch", + "main", + "--ignore", + ".venv", + "node_modules", + ] + ) + assert rc == 0 + assert captured["path_or_url"] == "/tmp/x" + assert captured["branch"] == "main" + assert captured["ignore"] == [".venv", "node_modules"] + + +# ── heavy end-to-end test ───────────────────────────────────────────── + + +@pytest.mark.skipif( + not _mcp_server_available(), + reason="api.mcp.server not present — requires MCP stack to be merged", +) +@pytest.mark.skipif(not _falkordb_reachable(), reason="FalkorDB unreachable") +def test_cg_mcp_search_code_end_to_end(tmp_path): + """Spawn the actual cg-mcp shim against a freshly-indexed fixture.""" + fixture = REPO_ROOT / "tests" / "mcp" / "fixtures" / "sample_project" + if not fixture.exists(): + pytest.skip("MCP sample fixture not present") + + env = os.environ.copy() + env["FALKORDB_HOST"] = os.environ.get("FALKORDB_HOST", "127.0.0.1") + env["FALKORDB_PORT"] = os.environ.get("FALKORDB_PORT", "6390") + env["BENCH_PYTHON"] = sys.executable + # Ensure cgraph-mcp is on PATH for the spawned subprocess. + venv_bin = str(Path(sys.executable).parent) + env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}" + + # Index the fixture under a deterministic project/branch. + project = "sample_project" + branch = f"benchmcp-{os.getpid()}" + idx = subprocess.run( + [ + str(REPO_ROOT / "bench" / "cli" / "cg-mcp"), + "index_repo", + "--path-or-url", + str(fixture), + "--branch", + branch, + ], + env=env, + capture_output=True, + text=True, + timeout=120, + ) + assert idx.returncode == 0, idx.stderr + idx_payload = json.loads(idx.stdout) + assert "graph_name" in idx_payload + assert idx_payload["num_nodes"] > 0 + + # Then search for any known symbol from the fixture. + sr = subprocess.run( + [ + str(REPO_ROOT / "bench" / "cli" / "cg-mcp"), + "search_code", + "--project", + project, + "--branch", + branch, + "--prefix", + "a", # broad prefix to match something in the fixture + "--limit", + "3", + ], + env=env, + capture_output=True, + text=True, + timeout=60, + ) + assert sr.returncode == 0, sr.stderr + out = json.loads(sr.stdout) + assert out is not None From f17d437b3abf856b96494890965ea63e5a169d83 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 14:51:38 +0300 Subject: [PATCH 37/63] bench: wire code_graph_mcp into mini_runner dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The adapter and shim from the previous commit were inert from the runner's perspective — VALID_CONFIGS only knew baseline/lsp/code_graph. This commit makes `--config code_graph_mcp` a first-class track. Changes in bench/runners/mini_runner.py: - VALID_CONFIGS gains "code_graph_mcp" (passes argparse + help string). - New INSTANCE_TEMPLATE_CODE_GRAPH_MCP: mirrors the HTTP code_graph template but tells the agent to call `cg-mcp` with $PROJECT_NAME + $BRANCH, and to use impact_analysis before non-trivial edits. - load_instance_template dispatches the new template. - config_env("code_graph_mcp", ...) prepends venv bin to PATH (so cgraph-mcp is callable from the agent's bash), passes FALKORDB_* through to the spawned MCP server, and exports PROJECT_NAME + BRANCH which the preamble references. - New _ensure_indexed_mcp() mirrors _ensure_indexed but goes through the bench MCP adapter instead of HTTP. Skip-if-present probe hits FalkorDB's GRAPH.LIST directly (one trip, no MCP spawn). - Per-instance loop now dispatches to _ensure_indexed_mcp for the new config. Smoke-verified that: - VALID_CONFIGS == ('baseline','lsp','code_graph','code_graph_mcp') - load_instance_template('code_graph_mcp') contains 'cg-mcp' - config_env populates PROJECT_NAME/BRANCH/FALKORDB_HOST Unit tests for the adapter still pass (5 passed, 1 skipped — heavy e2e double-gated on FalkorDB + api.mcp.server availability). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 87 +++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 3689c0aa..17b0cbaa 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -49,7 +49,7 @@ DEFAULT_CACHE_DIR = BENCH_DIR / "cache" DEFAULT_RESULTS = DEFAULT_CACHE_DIR / "results.jsonl" -VALID_CONFIGS = ("baseline", "lsp", "code_graph") +VALID_CONFIGS = ("baseline", "lsp", "code_graph", "code_graph_mcp") # --------------------------------------------------------------------------- @@ -155,11 +155,40 @@ class Task: """ +INSTANCE_TEMPLATE_CODE_GRAPH_MCP = """\ +You are working in the repository at {{cwd}}. +The code-graph MCP server has already indexed this repository under the +project name `$PROJECT_NAME` on branch `$BRANCH` (use the env vars +literally). + +The task to solve: + +{{task}} + +**Required workflow.** Before reading or editing any file, your first +bash command MUST be: + + `cg-mcp search_code --project "$PROJECT_NAME" --branch "$BRANCH" --prefix ` + +Then use `cg-mcp get_callers --project "$PROJECT_NAME" --branch "$BRANCH" --symbol-id ` +to expand relationships before doing any textual search. Use +`cg-mcp impact_analysis ... --symbol-id --depth 3` before +non-trivial edits. + +When you believe the task is complete, finish your turn with a final +message that contains a unified diff of your changes inside a fenced +``` block, then exit. Do not commit; the harness reads the diff via +`git diff`. +""" + + def load_instance_template(config: str) -> str: if config == "lsp": return INSTANCE_TEMPLATE_LSP if config == "code_graph": return INSTANCE_TEMPLATE_CODE_GRAPH + if config == "code_graph_mcp": + return INSTANCE_TEMPLATE_CODE_GRAPH_MCP return INSTANCE_TEMPLATE @@ -210,6 +239,23 @@ def config_env(config: str, repo_path: Path) -> dict[str, str]: # The agent's preamble references $REPO_NAME — set it to the # worktree dirname, which is what analyze_folder used as the id. env["REPO_NAME"] = repo_path.name + elif config == "code_graph_mcp": + # MCP transport: agent calls `cg-mcp …` which spawns the + # `cgraph-mcp` stdio server per call. FalkorDB coordinates + # are passed through verbatim. + env.setdefault("FALKORDB_HOST", os.environ.get("FALKORDB_HOST", "127.0.0.1")) + env.setdefault("FALKORDB_PORT", os.environ.get("FALKORDB_PORT", "6379")) + # `cgraph-mcp` must be on PATH; the runner installs the + # falkordb-code-graph package into the same interpreter, so + # prepending the venv bin gives us the entry point. + venv_bin = str(Path(sys.executable).parent) + env["PATH"] = f"{venv_bin}:{env['PATH']}" + # The preamble references $PROJECT_NAME and $BRANCH; project + # name matches what `index_repo` derives from the folder + # (= worktree dirname), and branch is the per-instance tag we + # used when indexing. + env["PROJECT_NAME"] = repo_path.name + env["BRANCH"] = os.environ.get("CGRAPH_MCP_BRANCH", "_default") return env @@ -248,6 +294,41 @@ def _ensure_indexed(repo_path: Path) -> None: print(f"[index] WARN failed to index {repo_name}: {exc!r}") +def _ensure_indexed_mcp(repo_path: Path) -> None: + """MCP-track equivalent of _ensure_indexed. + + Drives the `index_repo` MCP tool in-process via the bench adapter + (avoids spawning a second cgraph-mcp just to bootstrap; the agent + will spawn its own per call). Same skip-if-present optimization + as the HTTP path: cheap GRAPH.LIST scan against FalkorDB. + """ + from bench.agents import code_graph_mcp_adapter as cgm + import redis + + repo_name = repo_path.name + branch = os.environ.get("CGRAPH_MCP_BRANCH", "_default") + host = os.environ.get("FALKORDB_HOST", "127.0.0.1") + port = int(os.environ.get("FALKORDB_PORT", "6379")) + expected_graph = f"code:{repo_name}:{branch}" + try: + r = redis.Redis(host=host, port=port, decode_responses=True, socket_timeout=2) + if expected_graph in (r.execute_command("GRAPH.LIST") or []): + print(f"[index-mcp] {expected_graph} already indexed; skip") + return + except Exception as exc: # noqa: BLE001 + print(f"[index-mcp] WARN list_graphs failed ({exc!r}); will attempt index anyway") + + print(f"[index-mcp] indexing {repo_path} as {expected_graph} ...") + try: + payload = cgm.index_repo(str(repo_path), branch=branch) + if isinstance(payload, dict) and payload.get("error"): + print(f"[index-mcp] WARN index_repo error: {payload['error']!r}") + else: + print(f"[index-mcp] indexed: {payload}") + except Exception as exc: # noqa: BLE001 + print(f"[index-mcp] WARN failed to index {repo_name}: {exc!r}") + + # --------------------------------------------------------------------------- # Dry-run stub model # --------------------------------------------------------------------------- @@ -569,7 +650,7 @@ def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(description="code-graph benchmark runner") p.add_argument("--config", choices=VALID_CONFIGS, action="append", - help="one of baseline / lsp / code_graph; repeatable. " + help="one of baseline / lsp / code_graph / code_graph_mcp; repeatable. " "Default: all three.") mode = p.add_mutually_exclusive_group(required=True) mode.add_argument("--dry-run", action="store_true", @@ -640,6 +721,8 @@ def main(argv: list[str] | None = None) -> int: # call returns nothing and the agent abandons the tool. if cfg == "code_graph": _ensure_indexed(cfg_wt) + elif cfg == "code_graph_mcp": + _ensure_indexed_mcp(cfg_wt) cfg_rows = run_batch( [task], [cfg], From b14432b9448c2b9cc3483b0f93a887bbc55e3813 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Wed, 27 May 2026 16:28:47 +0300 Subject: [PATCH 38/63] bench: fail loudly on indexing errors + bump analyze_folder timeout The mini_runner previously printed a [index] WARN line on analyze_folder errors and continued. This meant SWE-bench instances whose path falls outside ALLOWED_ANALYSIS_DIR (e.g. when the API server is started from a sibling worktree) would silently run the agent against a missing code-graph project. The agent's first cg call returns 400 'Missing project ...', the agent falls back to grep/sed, and we get a token count that looks bad for the code_graph track but actually reflects 'tool unavailable'. Two changes: * analyze_folder errors and httpx exceptions now raise RuntimeError with the offending path. This stops the run and surfaces the ALLOWED_ANALYSIS_DIR misconfiguration immediately. * analyze_folder timeout bumped 600s -> 1800s. The 600s default was tight for sympy (~5 MB of Python, ~5000 functions) and caused a timeout during indexing. This was discovered while running the first real 3-way SWE-bench smoke. With the fix and a corrected ALLOWED_ANALYSIS_DIR, the code_graph track produces sensible numbers (-11% input vs baseline across the smoke sample vs the prior bogus +4.7%). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 17b0cbaa..cb6b72a0 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -281,17 +281,19 @@ def _ensure_indexed(repo_path: Path) -> None: print(f"[index] {repo_name} already indexed; skip") return print(f"[index] analyzing {repo_path} ...") - with httpx.Client(timeout=600.0, headers=headers) as c: + with httpx.Client(timeout=1800.0, headers=headers) as c: r = c.post( f"{base}/api/analyze_folder", json={"path": str(repo_path), "ignore": []}, ) if r.status_code != 200: - print(f"[index] WARN analyze_folder returned {r.status_code}: {r.text[:200]}") - else: - print(f"[index] indexed {repo_name}") - except Exception as exc: # noqa: BLE001 - print(f"[index] WARN failed to index {repo_name}: {exc!r}") + raise RuntimeError( + f"analyze_folder returned {r.status_code}: {r.text[:300]}. " + f"Check ALLOWED_ANALYSIS_DIR on the API server covers {repo_path}." + ) + print(f"[index] indexed {repo_name}") + except Exception as exc: + raise RuntimeError(f"failed to index {repo_name} at {repo_path}: {exc}") from exc def _ensure_indexed_mcp(repo_path: Path) -> None: From 532d84957763cf1114681c5bdd75fc6c44889bfe Mon Sep 17 00:00:00 2001 From: dvirdukhan Date: Wed, 27 May 2026 16:51:42 +0300 Subject: [PATCH 39/63] fix(analyzer): resolve LSP CALLS edges on repos without a venv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python analyzer hardcoded `environment_path={path}/venv` when starting jedi-language-server via multilspy. When the repo had no venv (the common case for cloned codebases like sphinx, sympy, anything from SWE-bench), jedi raised `InvalidPythonEnvironment` on every `request_definition()` call. analyzer.resolve() then swallowed the exception silently and the indexer produced a graph with DEFINES edges only — zero CALLS, zero EXTENDS. Benchmark validation showed sphinx (5K functions) and sympy (41K functions) had no resolved cross-references at all. Fix: - source_analyzer.py: prefer {repo}/venv, then {repo}/.venv, then fall back to the host interpreter's environment (sys.executable's prefix) so jedi always has a valid Python to introspect. - analyzer.py: log resolve() failures at WARN with file/line context instead of swallowing them silently, so the next regression is loud. Verified: re-indexed sphinx-doc/sphinx-9230 with the fix: DEFINES: 5640, CALLS: 4931, EXTENDS: 484 (was DEFINES-only). Fixes #685. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/analyzer.py | 5 +++++ api/analyzers/source_analyzer.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/api/analyzers/analyzer.py b/api/analyzers/analyzer.py index 64d49004..0564606b 100644 --- a/api/analyzers/analyzer.py +++ b/api/analyzers/analyzer.py @@ -57,6 +57,11 @@ def resolve(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: P locations = lsp.request_definition(str(file_path), node.start_point.row, node.start_point.column) return [(files[Path(self.resolve_path(location['absolutePath'], path))], files[Path(self.resolve_path(location['absolutePath'], path))].tree.root_node.descendant_for_point_range(Point(location['range']['start']['line'], location['range']['start']['character']), Point(location['range']['end']['line'], location['range']['end']['character']))) for location in locations if location and Path(self.resolve_path(location['absolutePath'], path)) in files] except Exception as e: + import logging + logging.getLogger(__name__).warning( + "resolve() failed for %s @%d:%d: %s", + file_path, node.start_point.row, node.start_point.column, e, + ) return [] @abstractmethod diff --git a/api/analyzers/source_analyzer.py b/api/analyzers/source_analyzer.py index 4186f358..1b8f85b1 100644 --- a/api/analyzers/source_analyzer.py +++ b/api/analyzers/source_analyzer.py @@ -134,7 +134,27 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None: else: lsps[".java"] = NullLanguageServer() if any(path.rglob('*.py')): - config = MultilspyConfig.from_dict({"code_language": "python", "environment_path": f"{path}/venv"}) + import sys + py_venv = path / "venv" + py_dotvenv = path / ".venv" + if py_venv.is_dir() and (py_venv / "bin" / "python").exists(): + env_path = str(py_venv) + elif py_dotvenv.is_dir() and (py_dotvenv / "bin" / "python").exists(): + env_path = str(py_dotvenv) + else: + # Fall back to the host's Python environment so jedi has a + # valid interpreter to introspect; otherwise every + # request_definition() raises InvalidPythonEnvironment and + # we'd silently produce a graph with zero CALLS edges. + env_path = str(Path(sys.executable).resolve().parent.parent) + logging.info( + "No venv at %s; falling back to host env %s for jedi LSP", + path, env_path, + ) + config = MultilspyConfig.from_dict({ + "code_language": "python", + "environment_path": env_path, + }) lsps[".py"] = SyncLanguageServer.create(config, logger, str(path)) else: lsps[".py"] = NullLanguageServer() From 476bc73d4a3b029ee437e057e82bf05a64874230 Mon Sep 17 00:00:00 2001 From: dvirdukhan Date: Wed, 27 May 2026 20:25:26 +0300 Subject: [PATCH 40/63] bench: add resume support + ignore sympy rubi rules Two production-quality fixes from the calibration run that crashed at 14/30 trajectories: 1. Resume support: skip (instance, cfg) pairs whose trajectory file already exists. Lets us recover from crashes/kills without re-running completed work (avoids ~$3 of wasted compute on this run). 2. Ignore pathological files at index time: sympy/integrals/rubi/rules contains auto-generated 3000-line files with hundreds of unresolvable symbols per line. jedi spends hours and never makes progress. Adding it to the default ignore list unblocks sympy-19040 (and other sympy instances) without affecting graph quality. Also expanded default ignore set: __pycache__, build, dist, .tox, .eggs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index cb6b72a0..03e32e7b 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -281,10 +281,19 @@ def _ensure_indexed(repo_path: Path) -> None: print(f"[index] {repo_name} already indexed; skip") return print(f"[index] analyzing {repo_path} ...") + # Default ignore set: auto-generated / vendored / pathological dirs + # that either contain no useful symbols or send jedi into a + # multi-hour resolve loop (e.g. sympy/integrals/rubi/rules has + # 3000-line files with hundreds of unresolvable symbols per line). + default_ignore = [ + ".git", "venv", ".venv", "node_modules", "__pycache__", + "rubi/rules", # sympy: blocks indexing for ~hours otherwise + "build", "dist", ".tox", ".eggs", + ] with httpx.Client(timeout=1800.0, headers=headers) as c: r = c.post( f"{base}/api/analyze_folder", - json={"path": str(repo_path), "ignore": []}, + json={"path": str(repo_path), "ignore": default_ignore}, ) if r.status_code != 200: raise RuntimeError( @@ -708,6 +717,13 @@ def main(argv: list[str] | None = None) -> int: f"x {len(configs)} configs = {len(insts) * len(configs)} trajectories") for inst in insts: for cfg in configs: + # Resume support: if a trajectory file for this (instance, cfg) + # already exists, skip the run entirely. Lets us recover from + # crashes / kills without re-spending tokens on completed work. + existing_traj = args.trajectories / f"{inst.instance_id}__{cfg}.json" + if existing_traj.exists(): + print(f"[resume] {inst.instance_id}/{cfg}: trajectory exists, skip") + continue # Fresh worktree per (instance, config) to avoid cross-talk. wt = prepare_worktree(inst) # Rename so each cfg gets a distinct path. From d23ef79f80df790515309a28c15bf1590d9ef05f Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 07:29:12 +0300 Subject: [PATCH 41/63] fix(analyzer): defensive skip when second_pass references untracked file In source_analyzer.second_pass, the list of files we iterate can include paths that first_pass did not add to self.files (e.g. parse errors, LSP-induced timeouts, or rare edge cases where a candidate file is present in the input list but never makes it into the files map). Previously this raised KeyError and aborted the entire index. Hit on sympy/polys/distributedmodules.py during bench calibration of sympy-12481. Skip with a WARN log instead so a single bad file no longer takes down the whole index. Also bump mini_runner httpx timeout 1800s -> 7200s; observed sympy-12481 index taking >30 min in the field, which previously left the API server indexing successfully but the runner gave up early. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/source_analyzer.py | 11 ++++++++++- bench/runners/mini_runner.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/api/analyzers/source_analyzer.py b/api/analyzers/source_analyzer.py index 1b8f85b1..ead8707a 100644 --- a/api/analyzers/source_analyzer.py +++ b/api/analyzers/source_analyzer.py @@ -166,7 +166,16 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None: with lsps[".java"].start_server(), lsps[".py"].start_server(), lsps[".cs"].start_server(): files_len = len(self.files) for i, file_path in enumerate(files): - file = self.files[file_path] + file = self.files.get(file_path) + if file is None: + # first_pass skipped this file (e.g. parse error, empty, + # or ignored after entering the candidate list). Skip + # in second_pass too instead of crashing the whole index. + logging.warning( + "second_pass: %s not in files map (first_pass skipped it); skipping", + file_path, + ) + continue logging.info(f'Processing file ({i + 1}/{files_len}): {file_path}') for _, entity in file.entities.items(): entity.resolved_symbol(lambda key, symbol, fp=file_path: analyzers[fp.suffix].resolve_symbol(self.files, lsps[fp.suffix], fp, path, key, symbol)) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 03e32e7b..3081bd6e 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -290,7 +290,7 @@ def _ensure_indexed(repo_path: Path) -> None: "rubi/rules", # sympy: blocks indexing for ~hours otherwise "build", "dist", ".tox", ".eggs", ] - with httpx.Client(timeout=1800.0, headers=headers) as c: + with httpx.Client(timeout=7200.0, headers=headers) as c: r = c.post( f"{base}/api/analyze_folder", json={"path": str(repo_path), "ignore": default_ignore}, From 484170104fbb410304acce2972d1e456753f5044 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 08:51:59 +0300 Subject: [PATCH 42/63] refactor(analyzers): extract TreeSitterAnalyzer base class (T15 #663) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/javascript/analyzer.py | 84 ++++----------- api/analyzers/kotlin/analyzer.py | 61 +++++------ api/analyzers/python/analyzer.py | 70 ++++-------- api/analyzers/tree_sitter_base.py | 107 +++++++++++++++++++ tests/analyzers/__init__.py | 0 tests/analyzers/fixtures/multilang/sample.js | 2 + tests/analyzers/fixtures/multilang/sample.kt | 2 + tests/analyzers/fixtures/multilang/sample.py | 7 ++ tests/analyzers/test_tree_sitter_base.py | 77 +++++++++++++ 9 files changed, 267 insertions(+), 143 deletions(-) create mode 100644 api/analyzers/tree_sitter_base.py create mode 100644 tests/analyzers/__init__.py create mode 100644 tests/analyzers/fixtures/multilang/sample.js create mode 100644 tests/analyzers/fixtures/multilang/sample.kt create mode 100644 tests/analyzers/fixtures/multilang/sample.py create mode 100644 tests/analyzers/test_tree_sitter_base.py diff --git a/api/analyzers/javascript/analyzer.py b/api/analyzers/javascript/analyzer.py index abc2879f..d5833cb9 100644 --- a/api/analyzers/javascript/analyzer.py +++ b/api/analyzers/javascript/analyzer.py @@ -3,10 +3,8 @@ from pathlib import Path from typing import Optional -from multilspy import SyncLanguageServer from ...entities.entity import Entity -from ...entities.file import File -from ..analyzer import AbstractAnalyzer +from ..tree_sitter_base import TreeSitterAnalyzer import tree_sitter_javascript as tsjs from tree_sitter import Language, Node @@ -15,13 +13,28 @@ logger = logging.getLogger('code_graph') -class JavaScriptAnalyzer(AbstractAnalyzer): +class JavaScriptAnalyzer(TreeSitterAnalyzer): """Analyzer for JavaScript source files using tree-sitter. Extracts functions, classes, and methods from JavaScript code. Resolves class inheritance (extends) and function/method call references. """ + entity_node_types = { + 'function_declaration': "Function", + 'class_declaration': "Class", + 'method_definition': "Method", + } + type_definition_node_types = ('class_declaration',) + callable_definition_node_types = ( + 'function_declaration', + 'method_definition', + 'class_declaration', + ) + callable_exclude_node_types = ('class_declaration',) + type_resolution_keys = ("base_class",) + method_resolution_keys = ("call",) + def __init__(self) -> None: """Initialize the JavaScript analyzer with the tree-sitter JS grammar.""" super().__init__(Language(tsjs.language())) @@ -33,26 +46,6 @@ def add_dependencies(self, path: Path, files: list[Path]) -> None: """ pass - def get_entity_label(self, node: Node) -> str: - """Return the graph label for a given AST node type. - - Args: - node: A tree-sitter AST node representing a JavaScript entity. - - Returns: - One of 'Function', 'Class', or 'Method'. - - Raises: - ValueError: If the node type is not a recognised entity. - """ - if node.type == 'function_declaration': - return "Function" - elif node.type == 'class_declaration': - return "Class" - elif node.type == 'method_definition': - return "Method" - raise ValueError(f"Unknown entity type: {node.type}") - def get_entity_name(self, node: Node) -> str: """Extract the declared name from a JavaScript entity node. @@ -92,10 +85,6 @@ def get_entity_docstring(self, node: Node) -> Optional[str]: return None raise ValueError(f"Unknown entity type: {node.type}") - def get_entity_types(self) -> list[str]: - """Return the tree-sitter node types recognised as JavaScript entities.""" - return ['function_declaration', 'class_declaration', 'method_definition'] - def add_symbols(self, entity: Entity) -> None: """Extract symbols (references) from a JavaScript entity. @@ -128,45 +117,12 @@ def is_dependency(self, file_path: str) -> bool: """ return "node_modules" in Path(file_path).parts - def resolve_path(self, file_path: str, path: Path) -> str: - """Resolve an import path relative to the project root.""" - return file_path - - def resolve_type(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: - """Resolve a type reference to its class declaration entity.""" - res = [] - for file, resolved_node in self.resolve(files, lsp, file_path, path, node): - type_dec = self.find_parent(resolved_node, ['class_declaration']) - if type_dec in file.entities: - res.append(file.entities[type_dec]) - return res - - def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: - """Resolve a call expression to the target function or method entity.""" - res = [] + def _extract_call_target(self, node: Node) -> Optional[Node]: + """Extract the callable target from a JavaScript call expression.""" if node.type == 'call_expression': func_node = node.child_by_field_name('function') if func_node and func_node.type == 'member_expression': func_node = func_node.child_by_field_name('property') if func_node: node = func_node - for file, resolved_node in self.resolve(files, lsp, file_path, path, node): - method_dec = self.find_parent(resolved_node, ['function_declaration', 'method_definition', 'class_declaration']) - if method_dec and method_dec.type == 'class_declaration': - continue - if method_dec in file.entities: - res.append(file.entities[method_dec]) - return res - - def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]: - """Dispatch symbol resolution based on the symbol category. - - Routes ``base_class`` symbols to type resolution and ``call`` symbols - to method resolution. - """ - if key == "base_class": - return self.resolve_type(files, lsp, file_path, path, symbol) - elif key == "call": - return self.resolve_method(files, lsp, file_path, path, symbol) - else: - raise ValueError(f"Unknown key {key}") + return node diff --git a/api/analyzers/kotlin/analyzer.py b/api/analyzers/kotlin/analyzer.py index 3758c302..208a51af 100644 --- a/api/analyzers/kotlin/analyzer.py +++ b/api/analyzers/kotlin/analyzer.py @@ -2,7 +2,7 @@ from ...entities.entity import Entity from ...entities.file import File from typing import Optional -from ..analyzer import AbstractAnalyzer +from ..tree_sitter_base import TreeSitterAnalyzer from multilspy import SyncLanguageServer @@ -12,7 +12,27 @@ import logging logger = logging.getLogger('code_graph') -class KotlinAnalyzer(AbstractAnalyzer): +class KotlinAnalyzer(TreeSitterAnalyzer): + entity_node_types = { + 'class_declaration': "Class", + 'object_declaration': "Object", + 'function_declaration': "Function", + } + type_definition_node_types = ('class_declaration', 'object_declaration') + callable_definition_node_types = ( + 'function_declaration', + 'class_declaration', + 'object_declaration', + ) + callable_exclude_node_types = ('class_declaration', 'object_declaration') + type_resolution_keys = ( + "implement_interface", + "base_class", + "parameters", + "return_type", + ) + method_resolution_keys = ("call",) + def __init__(self) -> None: super().__init__(Language(tskotlin.language())) @@ -44,7 +64,7 @@ def get_entity_name(self, node: Node) -> str: if child.type == 'identifier': return child.text.decode('utf-8') raise ValueError(f"Cannot extract name from entity type: {node.type}") - + def get_entity_docstring(self, node: Node) -> Optional[str]: if node.type in ['class_declaration', 'object_declaration', 'function_declaration']: # Check for KDoc comment (/** ... */) before the node @@ -54,14 +74,11 @@ def get_entity_docstring(self, node: Node) -> Optional[str]: if comment_text.startswith('/**'): return comment_text return None - raise ValueError(f"Unknown entity type: {node.type}") + raise ValueError(f"Unknown entity type: {node.type}") - def get_entity_types(self) -> list[str]: - return ['class_declaration', 'object_declaration', 'function_declaration'] - def _get_delegation_types(self, entity: Entity) -> list[tuple]: """Extract type identifiers from delegation specifiers in order. - + Returns list of (node, is_constructor_invocation) tuples. constructor_invocation indicates a superclass; plain user_type indicates an interface. """ @@ -91,25 +108,25 @@ def add_symbols(self, entity: Entity) -> None: entity.add_symbol("base_class", node) else: entity.add_symbol("implement_interface", node) - + elif entity.node.type == 'object_declaration': types = self._get_delegation_types(entity) for node, _ in types: entity.add_symbol("implement_interface", node) - + elif entity.node.type == 'function_declaration': # Find function calls captures = self._captures("(call_expression) @reference.call", entity.node) if 'reference.call' in captures: for caller in captures['reference.call']: entity.add_symbol("call", caller) - + # Find parameters with types captures = self._captures("(parameter (user_type (identifier) @parameter))", entity.node) if 'parameter' in captures: for parameter in captures['parameter']: entity.add_symbol("parameters", parameter) - + # Find return type captures = self._captures("(function_declaration (user_type (identifier) @return_type))", entity.node) if 'return_type' in captures: @@ -120,18 +137,6 @@ def is_dependency(self, file_path: str) -> bool: # Check if file is in a dependency directory (e.g., build, .gradle cache) return "build/" in file_path or ".gradle/" in file_path or "/cache/" in file_path - def resolve_path(self, file_path: str, path: Path) -> str: - # For Kotlin, just return the file path as-is for now - return file_path - - def resolve_type(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: - res = [] - for file, resolved_node in self.resolve(files, lsp, file_path, path, node): - type_dec = self.find_parent(resolved_node, ['class_declaration', 'object_declaration']) - if type_dec in file.entities: - res.append(file.entities[type_dec]) - return res - def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: res = [] # For call expressions, we need to extract the function name @@ -147,11 +152,3 @@ def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_ res.append(file.entities[method_dec]) break return res - - def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]: - if key in ["implement_interface", "base_class", "parameters", "return_type"]: - return self.resolve_type(files, lsp, file_path, path, symbol) - elif key in ["call"]: - return self.resolve_method(files, lsp, file_path, path, symbol) - else: - raise ValueError(f"Unknown key {key}") diff --git a/api/analyzers/python/analyzer.py b/api/analyzers/python/analyzer.py index 7a991202..7757ff60 100644 --- a/api/analyzers/python/analyzer.py +++ b/api/analyzers/python/analyzer.py @@ -1,12 +1,12 @@ import os import subprocess -from multilspy import SyncLanguageServer from pathlib import Path import tomllib -from ...entities import * from typing import Optional -from ..analyzer import AbstractAnalyzer + +from ...entities.entity import Entity +from ..tree_sitter_base import TreeSitterAnalyzer import tree_sitter_python as tspython from tree_sitter import Language, Node @@ -14,10 +14,19 @@ import logging logger = logging.getLogger('code_graph') -class PythonAnalyzer(AbstractAnalyzer): +class PythonAnalyzer(TreeSitterAnalyzer): + entity_node_types = { + 'class_definition': "Class", + 'function_definition': "Function", + } + type_definition_node_types = ('class_definition',) + callable_definition_node_types = ('function_definition', 'class_definition') + type_resolution_keys = ("base_class", "parameters", "return_type") + method_resolution_keys = ("call",) + def __init__(self) -> None: super().__init__(Language(tspython.language())) - + def add_dependencies(self, path: Path, files: list[Path]): if Path(f"{path}/venv").is_dir(): return @@ -40,18 +49,11 @@ def add_dependencies(self, path: Path, files: list[Path]): for requirement in requirements: files.extend(Path(f"{path}/venv/lib/").rglob(f"**/site-packages/{requirement}/*.py")) - def get_entity_label(self, node: Node) -> str: - if node.type == 'class_definition': - return "Class" - elif node.type == 'function_definition': - return "Function" - raise ValueError(f"Unknown entity type: {node.type}") - def get_entity_name(self, node: Node) -> str: if node.type in ['class_definition', 'function_definition']: return node.child_by_field_name('name').text.decode('utf-8') raise ValueError(f"Unknown entity type: {node.type}") - + def get_entity_docstring(self, node: Node) -> Optional[str]: if node.type in ['class_definition', 'function_definition']: body = node.child_by_field_name('body') @@ -59,11 +61,8 @@ def get_entity_docstring(self, node: Node) -> Optional[str]: docstring_node = body.children[0].child(0) return docstring_node.text.decode('utf-8') return None - raise ValueError(f"Unknown entity type: {node.type}") - - def get_entity_types(self) -> list[str]: - return ['class_definition', 'function_definition'] - + raise ValueError(f"Unknown entity type: {node.type}") + def add_symbols(self, entity: Entity) -> None: if entity.node.type == 'class_definition': superclasses = entity.node.child_by_field_name("superclasses") @@ -88,37 +87,14 @@ def add_symbols(self, entity: Entity) -> None: def is_dependency(self, file_path: str) -> bool: return "venv" in file_path - def resolve_path(self, file_path: str, path: Path) -> str: - return file_path - - def resolve_type(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path, node: Node) -> list[Entity]: - res = [] + def _extract_type_target(self, node: Node) -> Optional[Node]: if node.type == 'attribute': - node = node.child_by_field_name('attribute') - for file, resolved_node in self.resolve(files, lsp, file_path, path, node): - type_dec = self.find_parent(resolved_node, ['class_definition']) - if type_dec in file.entities: - res.append(file.entities[type_dec]) - return res + return node.child_by_field_name('attribute') + return node - def resolve_method(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, node: Node) -> list[Entity]: - res = [] + def _extract_call_target(self, node: Node) -> Optional[Node]: if node.type == 'call': node = node.child_by_field_name('function') - if node.type == 'attribute': + if node and node.type == 'attribute': node = node.child_by_field_name('attribute') - for file, resolved_node in self.resolve(files, lsp, file_path, path, node): - method_dec = self.find_parent(resolved_node, ['function_definition', 'class_definition']) - if not method_dec: - continue - if method_dec in file.entities: - res.append(file.entities[method_dec]) - return res - - def resolve_symbol(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: Path, path: Path, key: str, symbol: Node) -> list[Entity]: - if key in ["base_class", "parameters", "return_type"]: - return self.resolve_type(files, lsp, file_path, path, symbol) - elif key in ["call"]: - return self.resolve_method(files, lsp, file_path, path, symbol) - else: - raise ValueError(f"Unknown key {key}") + return node diff --git a/api/analyzers/tree_sitter_base.py b/api/analyzers/tree_sitter_base.py new file mode 100644 index 00000000..459ccc38 --- /dev/null +++ b/api/analyzers/tree_sitter_base.py @@ -0,0 +1,107 @@ +"""Shared base class for tree-sitter-backed analyzers.""" + +from pathlib import Path +from typing import Optional + +from multilspy import SyncLanguageServer +from tree_sitter import Node + +from api.entities.entity import Entity +from api.entities.file import File + +from .analyzer import AbstractAnalyzer + + +class TreeSitterAnalyzer(AbstractAnalyzer): + """Base implementation for analyzers that use tree-sitter plus LSP resolution. + + Subclasses declare the node types they treat as graph entities and the symbol + keys that resolve to type or callable definitions. Language-specific AST + normalization can be implemented by overriding the target-extraction hooks. + """ + + entity_node_types: dict[str, str] = {} + type_definition_node_types: tuple[str, ...] = () + callable_definition_node_types: tuple[str, ...] = () + callable_exclude_node_types: tuple[str, ...] = () + type_resolution_keys: tuple[str, ...] = () + method_resolution_keys: tuple[str, ...] = () + + def resolve_path(self, file_path: str, path: Path) -> str: + """Resolve an LSP path into the key used by the analyzed file map.""" + return file_path + + def get_entity_types(self) -> list[str]: + """Return the tree-sitter node types recognized as graph entities.""" + return list(self.entity_node_types.keys()) + + def get_entity_label(self, node: Node) -> str: + """Return the graph label for an entity node declared by the subclass.""" + try: + return self.entity_node_types[node.type] + except KeyError as exc: + raise ValueError(f"Unknown entity type: {node.type}") from exc + + def resolve_symbol( + self, + files: dict[Path, File], + lsp: SyncLanguageServer, + file_path: Path, + path: Path, + key: str, + symbol: Node, + ) -> list[Entity]: + """Dispatch a captured symbol to type or callable resolution.""" + if key in self.type_resolution_keys: + return self.resolve_type(files, lsp, file_path, path, symbol) + if key in self.method_resolution_keys: + return self.resolve_method(files, lsp, file_path, path, symbol) + raise ValueError(f"Unknown key {key}") + + def _extract_call_target(self, node: Node) -> Optional[Node]: + """Normalize a call symbol before resolving it to a callable definition.""" + return node + + def _extract_type_target(self, node: Node) -> Optional[Node]: + """Normalize a type symbol before resolving it to a type definition.""" + return node + + def resolve_type( + self, + files: dict[Path, File], + lsp: SyncLanguageServer, + file_path: Path, + path: Path, + node: Node, + ) -> list[Entity]: + """Resolve a type reference to matching type-definition entities.""" + res = [] + target = self._extract_type_target(node) + if target is None: + return res + for file, resolved_node in self.resolve(files, lsp, file_path, path, target): + type_dec = self.find_parent(resolved_node, self.type_definition_node_types) + if type_dec in file.entities: + res.append(file.entities[type_dec]) + return res + + def resolve_method( + self, + files: dict[Path, File], + lsp: SyncLanguageServer, + file_path: Path, + path: Path, + node: Node, + ) -> list[Entity]: + """Resolve a call reference to matching callable-definition entities.""" + res = [] + target = self._extract_call_target(node) + if target is None: + return res + for file, resolved_node in self.resolve(files, lsp, file_path, path, target): + method_dec = self.find_parent(resolved_node, self.callable_definition_node_types) + if method_dec and method_dec.type in self.callable_exclude_node_types: + continue + if method_dec in file.entities: + res.append(file.entities[method_dec]) + return res diff --git a/tests/analyzers/__init__.py b/tests/analyzers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/analyzers/fixtures/multilang/sample.js b/tests/analyzers/fixtures/multilang/sample.js new file mode 100644 index 00000000..77782571 --- /dev/null +++ b/tests/analyzers/fixtures/multilang/sample.js @@ -0,0 +1,2 @@ +class JsThing { run() { return jsHelper(); } } +function jsHelper() { return 1; } diff --git a/tests/analyzers/fixtures/multilang/sample.kt b/tests/analyzers/fixtures/multilang/sample.kt new file mode 100644 index 00000000..11d6e4a3 --- /dev/null +++ b/tests/analyzers/fixtures/multilang/sample.kt @@ -0,0 +1,2 @@ +class KtThing { fun run(): Int = ktHelper() } +fun ktHelper(): Int = 1 diff --git a/tests/analyzers/fixtures/multilang/sample.py b/tests/analyzers/fixtures/multilang/sample.py new file mode 100644 index 00000000..3277d993 --- /dev/null +++ b/tests/analyzers/fixtures/multilang/sample.py @@ -0,0 +1,7 @@ +class PyThing: + def run(self): + return py_helper() + + +def py_helper(): + return 1 diff --git a/tests/analyzers/test_tree_sitter_base.py b/tests/analyzers/test_tree_sitter_base.py new file mode 100644 index 00000000..e88976f9 --- /dev/null +++ b/tests/analyzers/test_tree_sitter_base.py @@ -0,0 +1,77 @@ +from collections import Counter +from pathlib import Path + +from api.analyzers.javascript.analyzer import JavaScriptAnalyzer +from api.analyzers.kotlin.analyzer import KotlinAnalyzer +from api.analyzers.python.analyzer import PythonAnalyzer +from api.analyzers.source_analyzer import SourceAnalyzer, analyzers +from api.entities.file import File + + +class MockGraph: + def __init__(self): + self._next_id = 1 + self.files = [] + self.entities = {} + self.edges = [] + + def add_file(self, file): + file.id = self._next_id + self._next_id += 1 + self.files.append(file) + + def add_entity(self, label, name, doc, path, src_start, src_end, props): + entity_id = self._next_id + self._next_id += 1 + self.entities[entity_id] = { + "label": label, + "name": name, + "doc": doc, + "path": path, + "src_start": src_start, + "src_end": src_end, + "props": props, + } + return entity_id + + def connect_entities(self, rel, src, dest, props=None): + self.edges.append((rel, src, dest, props)) + + +def test_tree_sitter_subclasses_expose_expected_entity_node_types(): + assert list(PythonAnalyzer.entity_node_types) == [ + 'class_definition', + 'function_definition', + ] + assert list(JavaScriptAnalyzer.entity_node_types) == [ + 'function_declaration', + 'class_declaration', + 'method_definition', + ] + assert list(KotlinAnalyzer.entity_node_types) == [ + 'class_declaration', + 'object_declaration', + 'function_declaration', + ] + + +def test_tree_sitter_multilanguage_fixture_graph_counts(): + source_analyzer = SourceAnalyzer() + graph = MockGraph() + fixture_dir = Path(__file__).parent / "fixtures" / "multilang" + + for file_path in sorted(fixture_dir.iterdir()): + analyzer = analyzers[file_path.suffix] + tree = analyzer.parser.parse(file_path.read_bytes()) + file = File(file_path, tree) + graph.add_file(file) + source_analyzer.create_hierarchy(file, analyzer, graph) + + assert len(graph.files) == 3 + assert len(graph.entities) == 9 + assert len(graph.files) + len(graph.entities) == 12 + assert len(graph.edges) == 9 + assert Counter(entity["label"] for entity in graph.entities.values()) == Counter( + {"Class": 3, "Function": 4, "Method": 2} + ) + assert Counter(edge[0] for edge in graph.edges) == Counter({"DEFINES": 9}) From 3e8935fab0ad06fcb56ed58a124c1686167565d6 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 09:12:22 +0300 Subject: [PATCH 43/63] feat(analyzers): tree-sitter Python symbol resolver (T18 #689) Replace jedi-based resolution with a pure tree-sitter static resolver behind CODE_GRAPH_PY_RESOLVER=tree_sitter. Default remains jedi for backwards compatibility. Benchmark on pytest-dev/pytest-6202 (204 files): - jedi: 247.1s wall, CALLS=1976, EXTENDS=71 - tree-sitter: 6.9s wall, CALLS=4833, EXTENDS=83 ~36x speedup, broader call recall (jedi returns None ~80% of the time). Mechanism: - TreeSitterPythonResolver builds a project-wide symbol table (top-level funcs/classes/assigns, class methods, import maps) keyed by id(files) for lazy construction. - Resolution: head lookup (local module -> import map -> cross-project bare-name fallback) + tail walk through attributes and class methods. - Handles relative imports, aliased imports, import-of-package, Optional[T]/generic_type subscript unwrapping. - AbstractAnalyzer.needs_lsp() hook + PythonAnalyzer override let source_analyzer skip LSP startup and venv setup entirely when the static resolver is active. This is where the wall-time win actually lives (jedi warm-up was ~240s of the 247s baseline). Closes #689. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/analyzer.py | 11 + api/analyzers/python/analyzer.py | 49 ++ api/analyzers/python/ts_resolver.py | 506 +++++++++++++++++++++ api/analyzers/source_analyzer.py | 10 +- tests/analyzers/test_ts_python_resolver.py | 251 ++++++++++ 5 files changed, 824 insertions(+), 3 deletions(-) create mode 100644 api/analyzers/python/ts_resolver.py create mode 100644 tests/analyzers/test_ts_python_resolver.py diff --git a/api/analyzers/analyzer.py b/api/analyzers/analyzer.py index 33ca5a2b..63202851 100644 --- a/api/analyzers/analyzer.py +++ b/api/analyzers/analyzer.py @@ -58,6 +58,17 @@ def resolve(self, files: dict[Path, File], lsp: SyncLanguageServer, file_path: P return [(files[Path(self.resolve_path(location['absolutePath'], path))], files[Path(self.resolve_path(location['absolutePath'], path))].tree.root_node.descendant_for_point_range(Point(location['range']['start']['line'], location['range']['start']['character']), Point(location['range']['end']['line'], location['range']['end']['character']))) for location in locations if location and Path(self.resolve_path(location['absolutePath'], path)) in files] except Exception: return [] + + def needs_lsp(self) -> bool: + """Whether this analyzer needs an LSP server started in second_pass. + + Defaults to True for backward compatibility with the original + jedi/multilspy-backed analyzers. Subclasses that resolve symbols + statically (e.g. the tree-sitter resolver in #689) override to + return False so the orchestrator can skip the expensive LSP + warm-up. + """ + return True @abstractmethod def add_dependencies(self, path: Path, files: list[Path]): diff --git a/api/analyzers/python/analyzer.py b/api/analyzers/python/analyzer.py index 7757ff60..8cdfe96e 100644 --- a/api/analyzers/python/analyzer.py +++ b/api/analyzers/python/analyzer.py @@ -5,8 +5,12 @@ import tomllib from typing import Optional +from multilspy import SyncLanguageServer + from ...entities.entity import Entity +from ...entities.file import File from ..tree_sitter_base import TreeSitterAnalyzer +from .ts_resolver import TreeSitterPythonResolver import tree_sitter_python as tspython from tree_sitter import Language, Node @@ -14,6 +18,11 @@ import logging logger = logging.getLogger('code_graph') + +_RESOLVER_ENV = "CODE_GRAPH_PY_RESOLVER" +_RESOLVER_TREE_SITTER = "tree_sitter" + + class PythonAnalyzer(TreeSitterAnalyzer): entity_node_types = { 'class_definition': "Class", @@ -26,8 +35,48 @@ class PythonAnalyzer(TreeSitterAnalyzer): def __init__(self) -> None: super().__init__(Language(tspython.language())) + # Resolver selection: 'tree_sitter' opts into the static project-wide + # resolver (issue #689). Default is the historical jedi/LSP path so + # behaviour is unchanged until explicitly enabled. + resolver_choice = os.environ.get(_RESOLVER_ENV, "").strip().lower() + if resolver_choice == _RESOLVER_TREE_SITTER: + self._ts_resolver: Optional[TreeSitterPythonResolver] = ( + TreeSitterPythonResolver(self.language) + ) + logger.info("PythonAnalyzer: tree-sitter static resolver enabled") + else: + self._ts_resolver = None + + def resolve( + self, + files: dict[Path, File], + lsp: SyncLanguageServer, + file_path: Path, + path: Path, + node: Node, + ) -> list[tuple[File, Node]]: + """Resolve a name node to ``(File, def_node)`` pairs. + + When ``CODE_GRAPH_PY_RESOLVER=tree_sitter`` is set, bypass the LSP + and use the project-wide static resolver. Otherwise fall through to + the default jedi-backed implementation in ``AbstractAnalyzer``. + """ + if self._ts_resolver is not None: + return self._ts_resolver.resolve(files, file_path, path, node) + return super().resolve(files, lsp, file_path, path, node) + + def needs_lsp(self) -> bool: + # When the tree-sitter resolver is active we don't touch the LSP, so + # the orchestrator can skip starting one. + return self._ts_resolver is None def add_dependencies(self, path: Path, files: list[Path]): + # When the tree-sitter resolver is active, we resolve statically + # against the in-project files only — installing the project's + # transitive Python deps just to feed jedi adds 10s–10min of + # zero-value pip work. Short-circuit it. + if self._ts_resolver is not None: + return if Path(f"{path}/venv").is_dir(): return subprocess.run(["python3", "-m", "venv", "venv"], cwd=str(path)) diff --git a/api/analyzers/python/ts_resolver.py b/api/analyzers/python/ts_resolver.py new file mode 100644 index 00000000..d6b60c79 --- /dev/null +++ b/api/analyzers/python/ts_resolver.py @@ -0,0 +1,506 @@ +"""Tree-sitter-based static symbol resolver for Python. + +A drop-in replacement for the jedi/multilspy round-trip used by +``PythonAnalyzer.resolve``. Builds a project-wide symbol table from the +already-parsed tree-sitter trees and answers ``request_definition``-style +queries by static name resolution. + +Selected at runtime via ``CODE_GRAPH_PY_RESOLVER=tree_sitter``. + +The resolver intentionally returns the same shape ``AbstractAnalyzer.resolve`` +returns: a list of ``(File, Node)`` tuples where ``Node`` is the definition's +tree-sitter node in the target file. This keeps the rest of the analyzer +pipeline (``resolve_type`` / ``resolve_method`` walking up to find_parent) +unchanged. + +What we resolve (Python-only): + +* Module-local names (function / class defined in the same file). +* ``from X import Y`` — resolves ``Y`` to a definition in module ``X``. +* ``from X import Y as Z`` — same, addressed by ``Z``. +* ``import X`` then ``X.Y`` — drills the dotted chain through the import map. +* ``import X as Z`` then ``Z.Y`` — same. +* Cross-project fallback by bare-name lookup (matches the rest of the + codebase's tolerance for missing types — jedi returns ``None`` here + ~80% of the time anyway). + +What we don't resolve (matches jedi's miss behavior): + +* Dynamic dispatch (``getattr``, metaclasses, monkey-patching). +* Type inference beyond direct ``x = Foo()`` assignment. +* Star imports. +* Cross-package imports outside the indexed project tree. +""" + +from __future__ import annotations + +import logging +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +from tree_sitter import Language, Node, QueryCursor + +from api.entities.file import File + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Symbol table data model +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class _Definition: + """A name defined somewhere in the project.""" + + file_path: Path + node: Node + kind: str # 'class' | 'func' | 'method' | 'var' + + +@dataclass +class _ModuleIndex: + """Per-file index of top-level definitions, imports, and class methods.""" + + module: str + file_path: Path + # Top-level name -> Definition + top_level: dict[str, _Definition] = field(default_factory=dict) + # Class name -> { method_name: Definition } + class_methods: dict[str, dict[str, _Definition]] = field(default_factory=dict) + # Local name -> dotted target module path + # ``import os`` -> {'os': 'os'} + # ``import os.path as op`` -> {'op': 'os.path'} + # ``from x.y import z`` -> {'z': 'x.y.z'} + # ``from x.y import z as w`` -> {'w': 'x.y.z'} + imports: dict[str, str] = field(default_factory=dict) + + +# --------------------------------------------------------------------------- +# Tree-sitter queries (compiled once per language instance) +# --------------------------------------------------------------------------- + + +_QUERY_TOP_LEVEL_FUNC = """ +(module (function_definition name: (identifier) @name) @def) +(module (decorated_definition + definition: (function_definition name: (identifier) @name)) @def) +""" + +_QUERY_TOP_LEVEL_CLASS = """ +(module (class_definition name: (identifier) @name) @def) +(module (decorated_definition + definition: (class_definition name: (identifier) @name)) @def) +""" + +_QUERY_TOP_LEVEL_ASSIGN = """ +(module (expression_statement (assignment left: (identifier) @name) @def)) +""" + +_QUERY_CLASS_METHODS = """ +(class_definition + name: (identifier) @class_name + body: (block (function_definition name: (identifier) @method_name) @method_def)) +(class_definition + name: (identifier) @class_name + body: (block (decorated_definition + definition: (function_definition name: (identifier) @method_name) @method_def))) +""" + +# Plain ``import x`` / ``import x.y`` / ``import x as y`` / ``import x.y as z``. +_QUERY_IMPORT = """ +(import_statement) @stmt +""" + +# ``from x import y`` / ``from x import y as z`` / ``from . import y`` / ``from .x import y``. +_QUERY_IMPORT_FROM = """ +(import_from_statement) @stmt +""" + + +class _Queries: + """Compiled tree-sitter queries for a given Language.""" + + def __init__(self, language: Language) -> None: + self.top_level_func = language.query(_QUERY_TOP_LEVEL_FUNC) + self.top_level_class = language.query(_QUERY_TOP_LEVEL_CLASS) + self.top_level_assign = language.query(_QUERY_TOP_LEVEL_ASSIGN) + self.class_methods = language.query(_QUERY_CLASS_METHODS) + self.imports = language.query(_QUERY_IMPORT) + self.imports_from = language.query(_QUERY_IMPORT_FROM) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _path_to_module(file_path: Path, project_root: Path) -> str: + """Convert ``project/pkg/sub/mod.py`` to ``pkg.sub.mod``. + + Returns the file path itself (stringified) if it lives outside the + project root — those files can still hold definitions but their module + name is informational only. + """ + try: + rel = file_path.relative_to(project_root) + except ValueError: + return str(file_path) + parts = list(rel.parts) + if parts and parts[-1].endswith(".py"): + parts[-1] = parts[-1][:-3] + if parts and parts[-1] == "__init__": + parts.pop() + return ".".join(parts) + + +def _dotted_name_text(node: Node) -> str: + """Reconstruct a dotted ``a.b.c`` string from a tree-sitter node.""" + return node.text.decode("utf-8") + + +def _captures(query, root: Node) -> dict[str, list[Node]]: + cursor = QueryCursor(query) + return cursor.captures(root) + + +# --------------------------------------------------------------------------- +# Public resolver +# --------------------------------------------------------------------------- + + +class TreeSitterPythonResolver: + """Project-wide resolver. Build once, query many times. + + The resolver caches the project symbol table keyed on ``id(files)`` — + when the analyzer passes a fresh ``files`` dict (new index run) we + rebuild lazily on the next call. This avoids holding a reference to + the dict across runs. + """ + + def __init__(self, language: Language) -> None: + self._language = language + self._queries = _Queries(language) + self._files_id: Optional[int] = None + self._files: Optional[dict[Path, File]] = None + self._project_root: Optional[Path] = None + # module name -> _ModuleIndex + self._modules: dict[str, _ModuleIndex] = {} + # file path -> module name (reverse lookup) + self._path_to_module: dict[Path, str] = {} + # name -> [_Definition, ...] (cross-project fallback) + self._by_name: dict[str, list[_Definition]] = defaultdict(list) + + # -- build --------------------------------------------------------------- + + def _ensure_built(self, files: dict[Path, File], project_root: Path) -> None: + if self._files_id == id(files) and self._project_root == project_root: + return + self._files_id = id(files) + self._files = files + self._project_root = project_root + self._modules.clear() + self._path_to_module.clear() + self._by_name.clear() + + for file_path, file in files.items(): + if file_path.suffix != ".py" or file.tree is None: + continue + module = _path_to_module(file_path, project_root) + mi = _ModuleIndex(module=module, file_path=file_path) + self._modules[module] = mi + self._path_to_module[file_path] = module + self._index_file(mi, file.tree.root_node) + + def _index_file(self, mi: _ModuleIndex, root: Node) -> None: + # Top-level functions + caps = _captures(self._queries.top_level_func, root) + names = caps.get("name", []) + defs = caps.get("def", []) + for name_node, def_node in zip(names, defs): + name = name_node.text.decode("utf-8") + d = _Definition(mi.file_path, _strip_decorator(def_node), "func") + mi.top_level[name] = d + self._by_name[name].append(d) + + # Top-level classes + caps = _captures(self._queries.top_level_class, root) + names = caps.get("name", []) + defs = caps.get("def", []) + for name_node, def_node in zip(names, defs): + name = name_node.text.decode("utf-8") + d = _Definition(mi.file_path, _strip_decorator(def_node), "class") + mi.top_level[name] = d + self._by_name[name].append(d) + + # Top-level assignments (for class aliases like ``Foo = OtherFoo``) + caps = _captures(self._queries.top_level_assign, root) + names = caps.get("name", []) + defs = caps.get("def", []) + for name_node, def_node in zip(names, defs): + name = name_node.text.decode("utf-8") + if name in mi.top_level: + continue + d = _Definition(mi.file_path, def_node, "var") + mi.top_level[name] = d + self._by_name[name].append(d) + + # Class methods + caps = _captures(self._queries.class_methods, root) + class_names = caps.get("class_name", []) + method_names = caps.get("method_name", []) + method_defs = caps.get("method_def", []) + for cls_node, mname_node, mdef_node in zip(class_names, method_names, method_defs): + class_name = cls_node.text.decode("utf-8") + method_name = mname_node.text.decode("utf-8") + d = _Definition(mi.file_path, _strip_decorator(mdef_node), "method") + mi.class_methods.setdefault(class_name, {})[method_name] = d + self._by_name[method_name].append(d) + + # Imports + self._index_imports(mi, root) + + def _index_imports(self, mi: _ModuleIndex, root: Node) -> None: + # ``import X`` statements + for stmt in _captures(self._queries.imports, root).get("stmt", []): + for child in stmt.named_children: + if child.type == "dotted_name": + name = child.text.decode("utf-8") + # ``import pkg.lib`` binds the *top* package name; users + # access ``pkg.lib.x`` via the package head. Map the head + # to itself so resolution walks pkg → lib → x naturally. + head = name.split(".")[0] + mi.imports[head] = head + elif child.type == "aliased_import": + dotted = child.child_by_field_name("name") + alias = child.child_by_field_name("alias") + if dotted and alias: + mi.imports[alias.text.decode("utf-8")] = dotted.text.decode("utf-8") + + # ``from X import Y`` statements + for stmt in _captures(self._queries.imports_from, root).get("stmt", []): + module_node = stmt.child_by_field_name("module_name") + if module_node is None: + continue + base_module = self._resolve_from_module(module_node, mi.module) + if base_module is None: + continue + # Each import target is a sibling after module_name + for child in stmt.named_children: + if child == module_node: + continue + if child.type == "dotted_name": + name = child.text.decode("utf-8") + short = name.split(".")[-1] + mi.imports[short] = f"{base_module}.{name}" + elif child.type == "aliased_import": + dotted = child.child_by_field_name("name") + alias = child.child_by_field_name("alias") + if dotted and alias: + mi.imports[alias.text.decode("utf-8")] = ( + f"{base_module}.{dotted.text.decode('utf-8')}" + ) + # Wildcard: ignored (matches jedi miss) + + def _resolve_from_module(self, module_node: Node, current_module: str) -> Optional[str]: + """Handle relative imports (``from . import x``) by climbing the package.""" + if module_node.type == "dotted_name": + return module_node.text.decode("utf-8") + if module_node.type == "relative_import": + # Count leading dots; resolve relative to current package. + text = module_node.text.decode("utf-8") + dot_count = 0 + for ch in text: + if ch == ".": + dot_count += 1 + else: + break + tail = text[dot_count:] + base_parts = current_module.split(".") + # `from . import x` from pkg.a -> base = pkg + # `from .. import x` from pkg.a -> base = '' + up = dot_count + base = base_parts[: max(0, len(base_parts) - up)] + if tail: + base.append(tail) + return ".".join(p for p in base if p) or None + return None + + # -- query --------------------------------------------------------------- + + def resolve( + self, + files: dict[Path, File], + file_path: Path, + project_root: Path, + node: Node, + ) -> list[tuple[File, Node]]: + """Resolve ``node`` (an identifier or dotted attribute) to definitions. + + Returns a list of ``(File, def_node)`` tuples matching the shape + produced by ``AbstractAnalyzer.resolve``. + """ + self._ensure_built(files, project_root) + parts = _node_to_dotted_parts(node) + if not parts: + return [] + current_module = self._path_to_module.get(file_path) + candidate_defs = self._lookup(current_module, parts) + out: list[tuple[File, Node]] = [] + for d in candidate_defs: + f = files.get(d.file_path) + if f is None: + continue + out.append((f, d.node)) + return out + + def _lookup(self, current_module: Optional[str], parts: list[str]) -> list[_Definition]: + if not parts: + return [] + head = parts[0] + tail = parts[1:] + + # 1. Local module top-level + if current_module and current_module in self._modules: + mi = self._modules[current_module] + if head in mi.top_level: + return self._walk_tail(mi.top_level[head], tail) + # 2. Local file's imports + if head in mi.imports: + imported = mi.imports[head] + # Append the dotted tail to the imported prefix and look up + # the result as a fully-qualified dotted name. This handles + # both ``from x import y`` (imported='x.y', tail=[]) + # and ``import pkg.lib`` (imported='pkg.lib', tail=['shared']). + full_dotted = ".".join([imported, *tail]) if tail else imported + target_def = self._lookup_dotted(full_dotted) + if target_def is not None: + return [target_def] + # If the imported path itself names a module, allow direct + # top-level lookup against that module. + if imported in self._modules and tail: + mi2 = self._modules[imported] + if tail[0] in mi2.top_level: + return self._walk_tail(mi2.top_level[tail[0]], tail[1:]) + + # 3. Cross-project bare-name fallback + if head in self._by_name: + # If there's a tail, try walking each candidate; otherwise return all hits. + if not tail: + return list(self._by_name[head]) + out = [] + for d in self._by_name[head]: + out.extend(self._walk_tail(d, tail)) + return out + + return [] + + def _lookup_dotted(self, dotted: str) -> Optional[_Definition]: + """Resolve a fully-qualified ``pkg.mod.Name`` to its _Definition.""" + if dotted in self._modules: + # A bare module — there's no single definition, just a namespace. + return None + # Try splitting from the right: longest prefix that's a module, suffix is symbol path. + parts = dotted.split(".") + for split in range(len(parts) - 1, 0, -1): + mod_candidate = ".".join(parts[:split]) + symbol_parts = parts[split:] + if mod_candidate in self._modules: + mi = self._modules[mod_candidate] + if symbol_parts[0] in mi.top_level: + return self._walk_tail_single(mi.top_level[symbol_parts[0]], symbol_parts[1:]) + return None + + def _walk_tail(self, start: _Definition, tail: list[str]) -> list[_Definition]: + """Walk a dotted-attribute tail from a starting definition. Returns list.""" + d = self._walk_tail_single(start, tail) + return [d] if d is not None else [] + + def _walk_tail_single(self, start: _Definition, tail: list[str]) -> Optional[_Definition]: + cur = start + for part in tail: + if cur.kind == "class": + class_name = self._class_name_for_def(cur) + if class_name is None: + return None + mi = self._modules.get(self._path_to_module.get(cur.file_path, "")) + if mi is None: + return None + methods = mi.class_methods.get(class_name, {}) + if part in methods: + cur = methods[part] + continue + return None + # Other kinds: can't drill further statically + return None + return cur + + @staticmethod + def _class_name_for_def(d: _Definition) -> Optional[str]: + if d.kind != "class": + return None + name_node = d.node.child_by_field_name("name") + if name_node is None: + # decorated_definition: drill in + for child in d.node.named_children: + if child.type == "class_definition": + name_node = child.child_by_field_name("name") + break + return name_node.text.decode("utf-8") if name_node else None + + +# --------------------------------------------------------------------------- +# Module helpers +# --------------------------------------------------------------------------- + + +def _strip_decorator(def_node: Node) -> Node: + """If ``def_node`` is a decorated_definition, return its inner definition. + + The rest of the analyzer expects ``class_definition`` / ``function_definition`` + nodes (those are what ``add_symbols`` traverses and what ``find_parent`` + looks for), so we unwrap decorators here. + """ + if def_node.type == "decorated_definition": + for child in def_node.named_children: + if child.type in ("class_definition", "function_definition"): + return child + return def_node + + +def _node_to_dotted_parts(node: Node) -> list[str]: + """Reduce a tree-sitter Python expression to its dotted name parts. + + Returns ``[]`` if the node isn't a name reference we can statically resolve. + """ + if node.type == "identifier": + return [node.text.decode("utf-8")] + if node.type == "attribute": + obj = node.child_by_field_name("object") + attr = node.child_by_field_name("attribute") + if obj is None or attr is None: + return [] + head_parts = _node_to_dotted_parts(obj) + if not head_parts: + return [] + return head_parts + [attr.text.decode("utf-8")] + if node.type == "call": + func = node.child_by_field_name("function") + return _node_to_dotted_parts(func) if func else [] + if node.type in ("subscript", "generic_type"): + # ``Optional[Node]`` / ``dict[Path, File]`` — resolve the outer name. + # tree-sitter-python uses ``generic_type`` for type annotations and + # ``subscript`` for runtime indexing expressions. + if node.type == "subscript": + inner = node.child_by_field_name("value") + else: + inner = node.named_children[0] if node.named_children else None + return _node_to_dotted_parts(inner) if inner else [] + if node.type == "type": + # ``type`` wraps the actual annotation expression. + inner = node.named_children[0] if node.named_children else None + return _node_to_dotted_parts(inner) if inner else [] + return [] diff --git a/api/analyzers/source_analyzer.py b/api/analyzers/source_analyzer.py index 9046abcf..49dd00b3 100644 --- a/api/analyzers/source_analyzer.py +++ b/api/analyzers/source_analyzer.py @@ -138,7 +138,7 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None: lsps[".java"] = SyncLanguageServer.create(config, logger, str(path)) else: lsps[".java"] = NullLanguageServer() - if any(path.rglob('*.py')): + if any(path.rglob('*.py')) and analyzers[".py"].needs_lsp(): config = MultilspyConfig.from_dict({"code_language": "python", "environment_path": f"{path}/venv"}) lsps[".py"] = SyncLanguageServer.create(config, logger, str(path)) else: @@ -157,8 +157,12 @@ def second_pass(self, graph: Graph, files: list[Path], path: Path) -> None: for i, file_path in enumerate(files): if file_path not in self.files: continue - # Skip symbol resolution when no real LSP is available - if isinstance(lsps.get(file_path.suffix), NullLanguageServer): + analyzer = analyzers.get(file_path.suffix) + # Skip symbol resolution when no real LSP is available *and* the + # analyzer can't resolve statically (e.g. tree-sitter resolver). + if isinstance(lsps.get(file_path.suffix), NullLanguageServer) and ( + analyzer is None or analyzer.needs_lsp() + ): continue file = self.files[file_path] logging.info(f'Processing file ({i + 1}/{files_len}): {file_path}') diff --git a/tests/analyzers/test_ts_python_resolver.py b/tests/analyzers/test_ts_python_resolver.py new file mode 100644 index 00000000..2e8d3621 --- /dev/null +++ b/tests/analyzers/test_ts_python_resolver.py @@ -0,0 +1,251 @@ +"""Unit tests for the tree-sitter Python resolver (T18 / #689).""" + +from __future__ import annotations + +import os +from pathlib import Path +from unittest import mock + +import pytest +import tree_sitter_python as tspython +from tree_sitter import Language, Parser + +from api.analyzers.python.ts_resolver import ( + TreeSitterPythonResolver, + _node_to_dotted_parts, + _path_to_module, +) +from api.entities.file import File + + +_PY = Language(tspython.language()) +_PARSER = Parser(_PY) + + +def _file_from(path: Path, source: str) -> File: + tree = _PARSER.parse(source.encode("utf-8")) + return File(path, tree) + + +def _find_call_node(tree_root, text: str): + """Find the first call node whose surface text matches ``text``.""" + stack = [tree_root] + while stack: + node = stack.pop() + if node.type == "call" and node.text.decode("utf-8").startswith(text): + return node + stack.extend(node.children) + raise AssertionError(f"call '{text}' not found") + + +def _find_name_node(tree_root, text: str): + stack = [tree_root] + while stack: + node = stack.pop() + if node.type == "identifier" and node.text.decode("utf-8") == text: + return node + stack.extend(node.children) + raise AssertionError(f"identifier '{text}' not found") + + +# --------------------------------------------------------------------------- +# _node_to_dotted_parts +# --------------------------------------------------------------------------- + + +def test_dotted_parts_identifier(): + tree = _PARSER.parse(b"foo") + name = tree.root_node.descendant_for_point_range((0, 0), (0, 3)) + assert _node_to_dotted_parts(name) == ["foo"] + + +def test_dotted_parts_attribute_chain(): + tree = _PARSER.parse(b"a.b.c") + # The whole expression as an attribute node + expr = tree.root_node.named_children[0].named_children[0] + assert _node_to_dotted_parts(expr) == ["a", "b", "c"] + + +def test_dotted_parts_subscript_unwrapping(): + # Optional[Node] in a type annotation context. tree-sitter-python wraps + # this as a ``type`` node containing a ``generic_type``. + tree = _PARSER.parse(b"x: Optional[Node] = None\n") + type_node = None + stack = [tree.root_node] + while stack: + n = stack.pop() + if n.type == "type": + type_node = n + break + stack.extend(n.children) + assert type_node is not None + assert _node_to_dotted_parts(type_node) == ["Optional"] + + +# --------------------------------------------------------------------------- +# _path_to_module +# --------------------------------------------------------------------------- + + +def test_path_to_module_basic(tmp_path: Path): + root = tmp_path + f = root / "pkg" / "sub" / "mod.py" + assert _path_to_module(f, root) == "pkg.sub.mod" + + +def test_path_to_module_package_init(tmp_path: Path): + root = tmp_path + f = root / "pkg" / "sub" / "__init__.py" + assert _path_to_module(f, root) == "pkg.sub" + + +def test_path_to_module_outside_root(tmp_path: Path): + root = tmp_path + f = Path("/elsewhere/foo.py") + assert _path_to_module(f, root) == "/elsewhere/foo.py" + + +# --------------------------------------------------------------------------- +# Resolver end-to-end +# --------------------------------------------------------------------------- + + +def _make_project(tmp_path: Path, layout: dict[str, str]) -> dict[Path, File]: + files: dict[Path, File] = {} + for rel, src in layout.items(): + p = tmp_path / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(src) + files[p.resolve()] = _file_from(p.resolve(), src) + return files + + +def test_resolver_local_module_function(tmp_path: Path): + files = _make_project( + tmp_path, + { + "mod.py": ( + "def helper():\n pass\n\n" + "def caller():\n helper()\n" + ), + }, + ) + r = TreeSitterPythonResolver(_PY) + mod_path = (tmp_path / "mod.py").resolve() + helper_call = _find_call_node(files[mod_path].tree.root_node, "helper(") + # Caller passes the call's identifier (after _extract_call_target). + func_ident = helper_call.child_by_field_name("function") + out = r.resolve(files, mod_path, tmp_path.resolve(), func_ident) + assert len(out) == 1 + file, def_node = out[0] + assert file.path == mod_path + assert def_node.type == "function_definition" + name = def_node.child_by_field_name("name").text.decode("utf-8") + assert name == "helper" + + +def test_resolver_from_import_resolution(tmp_path: Path): + files = _make_project( + tmp_path, + { + "lib.py": "def shared():\n return 1\n", + "app.py": "from lib import shared\n\ndef use():\n shared()\n", + }, + ) + r = TreeSitterPythonResolver(_PY) + app_path = (tmp_path / "app.py").resolve() + lib_path = (tmp_path / "lib.py").resolve() + call = _find_call_node(files[app_path].tree.root_node, "shared(") + out = r.resolve(files, app_path, tmp_path.resolve(), call.child_by_field_name("function")) + assert len(out) == 1 + assert out[0][0].path == lib_path + assert out[0][1].child_by_field_name("name").text.decode("utf-8") == "shared" + + +def test_resolver_aliased_import(tmp_path: Path): + files = _make_project( + tmp_path, + { + "lib.py": "def shared():\n return 1\n", + "app.py": "from lib import shared as s\n\ndef use():\n s()\n", + }, + ) + r = TreeSitterPythonResolver(_PY) + app_path = (tmp_path / "app.py").resolve() + call = _find_call_node(files[app_path].tree.root_node, "s(") + out = r.resolve(files, app_path, tmp_path.resolve(), call.child_by_field_name("function")) + assert len(out) == 1 + assert out[0][0].path == (tmp_path / "lib.py").resolve() + + +def test_resolver_import_dotted_then_attribute(tmp_path: Path): + files = _make_project( + tmp_path, + { + "pkg/__init__.py": "", + "pkg/lib.py": "def shared():\n return 1\n", + "app.py": "import pkg.lib\n\ndef use():\n pkg.lib.shared()\n", + }, + ) + r = TreeSitterPythonResolver(_PY) + app_path = (tmp_path / "app.py").resolve() + call = _find_call_node(files[app_path].tree.root_node, "pkg.lib.shared(") + # The call's function is the attribute chain pkg.lib.shared + func = call.child_by_field_name("function") + out = r.resolve(files, app_path, tmp_path.resolve(), func) + assert len(out) == 1 + assert out[0][0].path == (tmp_path / "pkg" / "lib.py").resolve() + + +def test_resolver_class_method_via_class_name(tmp_path: Path): + files = _make_project( + tmp_path, + { + "mod.py": ( + "class Foo:\n" + " def bar(self):\n" + " return 1\n\n" + "def caller():\n" + " Foo.bar(None)\n" + ), + }, + ) + r = TreeSitterPythonResolver(_PY) + mod = (tmp_path / "mod.py").resolve() + call = _find_call_node(files[mod].tree.root_node, "Foo.bar") + func = call.child_by_field_name("function") + out = r.resolve(files, mod, tmp_path.resolve(), func) + assert len(out) == 1 + assert out[0][1].child_by_field_name("name").text.decode("utf-8") == "bar" + + +def test_resolver_unknown_name_returns_empty(tmp_path: Path): + files = _make_project(tmp_path, {"mod.py": "x = totally_unknown_name\n"}) + r = TreeSitterPythonResolver(_PY) + mod = (tmp_path / "mod.py").resolve() + name = _find_name_node(files[mod].tree.root_node, "totally_unknown_name") + assert r.resolve(files, mod, tmp_path.resolve(), name) == [] + + +# --------------------------------------------------------------------------- +# PythonAnalyzer integration via env var +# --------------------------------------------------------------------------- + + +def test_python_analyzer_disables_lsp_under_tree_sitter_env(): + with mock.patch.dict(os.environ, {"CODE_GRAPH_PY_RESOLVER": "tree_sitter"}): + from api.analyzers.python.analyzer import PythonAnalyzer + + a = PythonAnalyzer() + assert a._ts_resolver is not None + assert a.needs_lsp() is False + + +def test_python_analyzer_default_still_uses_jedi(): + with mock.patch.dict(os.environ, {}, clear=False): + os.environ.pop("CODE_GRAPH_PY_RESOLVER", None) + from api.analyzers.python.analyzer import PythonAnalyzer + + a = PythonAnalyzer() + assert a._ts_resolver is None + assert a.needs_lsp() is True From 612b04fcdb555b300052c9729db7175bff999743 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 09:16:11 +0300 Subject: [PATCH 44/63] perf(analyzers): memoise compiled tree-sitter queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AbstractAnalyzer._captures was recompiling its query string on every call. cProfile on pytest-dev/pytest-6202 (204 files) showed tree_sitter.Language.query consuming 3.03s of the 6.36s first_pass — ~48% of analyzer time spent rebuilding queries that never change. Cache them on the analyzer instance, keyed by pattern string. Also switches from the deprecated language.query() to the Query(language, pattern) constructor. Wall-time on pytest-6202 (CODE_GRAPH_PY_RESOLVER=tree_sitter): before: 6.9s after: 3.7s Benefits every tree-sitter analyzer (Python, JavaScript, Kotlin), not just the new static resolver. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/analyzers/analyzer.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api/analyzers/analyzer.py b/api/analyzers/analyzer.py index 63202851..137a4478 100644 --- a/api/analyzers/analyzer.py +++ b/api/analyzers/analyzer.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional -from tree_sitter import Language, Node, Parser, Point, QueryCursor +from tree_sitter import Language, Node, Parser, Point, Query, QueryCursor from api.entities.entity import Entity from api.entities.file import File from abc import ABC, abstractmethod @@ -11,11 +11,20 @@ class AbstractAnalyzer(ABC): def __init__(self, language: Language) -> None: self.language = language self.parser = Parser(language) + # Memoise compiled queries; tree-sitter query compilation is ~370us + # each and adds up to seconds on large repos. + self._query_cache: dict[str, Query] = {} + + def _get_query(self, pattern: str) -> Query: + q = self._query_cache.get(pattern) + if q is None: + q = Query(self.language, pattern) + self._query_cache[pattern] = q + return q def _captures(self, pattern: str, node: Node) -> dict: """Run a tree-sitter query and return captures dict.""" - query = self.language.query(pattern) - cursor = QueryCursor(query) + cursor = QueryCursor(self._get_query(pattern)) return cursor.captures(node) def find_parent(self, node: Node, parent_types: list) -> Node: From ec7fac6cc80e9bbdbfd6dc20696696fbb87fe35c Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 09:19:47 +0300 Subject: [PATCH 45/63] bench: add start-api.sh helper enabling tree-sitter fast resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After T18 (#691) + query-cache (#692), code_graph indexing on pytest-6202 drops from 247s to 3.7s — but only if the API server is launched with CODE_GRAPH_PY_RESOLVER=tree_sitter. This helper bakes in that env plus the public/permissive flags the bench harness expects, so calibration runs hit the fast path without manual setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/scripts/start-api.sh | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 bench/scripts/start-api.sh diff --git a/bench/scripts/start-api.sh b/bench/scripts/start-api.sh new file mode 100755 index 00000000..4e55f673 --- /dev/null +++ b/bench/scripts/start-api.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Launch the code-graph API server with the fast tree-sitter Python +# resolver enabled (PR #691 + #692). This is what the bench harness +# expects to talk to at 127.0.0.1:5000. +# +# Usage: +# bench/scripts/start-api.sh # default port 5000 +# bench/scripts/start-api.sh --port 5001 +# +# Prereqs: +# - FalkorDB running. For native falkordb on 6380 set +# FALKORDB_HOST=127.0.0.1 FALKORDB_PORT=6380 before invoking. +# - uv on PATH. +# - cwd must be a code-graph worktree containing api/ with PR #691 +# and PR #692 applied (i.e. the dvirdukhan/query-cache branch tip +# or staging once those are merged). + +set -euo pipefail + +PORT=5000 +while [[ $# -gt 0 ]]; do + case "$1" in + --port) PORT="$2"; shift 2 ;; + *) echo "Unknown arg: $1" >&2; exit 1 ;; + esac +done + +# Tree-sitter static resolver — turns Python indexing from minutes to +# seconds. Default is still jedi, so callers must opt in explicitly. +export CODE_GRAPH_PY_RESOLVER="${CODE_GRAPH_PY_RESOLVER:-tree_sitter}" + +# Allow the bench harness to analyze any folder; the bench worktrees +# live under bench/cache/worktrees. +export ALLOWED_ANALYSIS_DIR="${ALLOWED_ANALYSIS_DIR:-/}" + +# Public mode: bench harness does not bother with bearer tokens. +export CODE_GRAPH_PUBLIC="${CODE_GRAPH_PUBLIC:-1}" + +echo "[start-api] CODE_GRAPH_PY_RESOLVER=$CODE_GRAPH_PY_RESOLVER" +echo "[start-api] CODE_GRAPH_PUBLIC=$CODE_GRAPH_PUBLIC" +echo "[start-api] FALKORDB_HOST=${FALKORDB_HOST:-127.0.0.1} FALKORDB_PORT=${FALKORDB_PORT:-6379}" +echo "[start-api] Listening on 127.0.0.1:$PORT" + +exec uv run uvicorn api.index:app --host 127.0.0.1 --port "$PORT" From 5e6c63cfaffb03fcc390f8026eddfc22eaefc05d Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 10:02:28 +0300 Subject: [PATCH 46/63] fix(mcp): lazy-import KnowledgeGraph so server starts on graphrag 1.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After merging the bench harness (graphrag-sdk 1.1.1) with the MCP suite (written against 0.8 KnowledgeGraph), the server failed at import. Move the SDK import inside get_or_create_kg so only the 'ask' tool trips the incompatibility — structural tools used by the bench harness (index_repo, search_code, get_callers, ...) work either way. --- api/mcp/graphrag_init.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/api/mcp/graphrag_init.py b/api/mcp/graphrag_init.py index b0341e77..20196a82 100644 --- a/api/mcp/graphrag_init.py +++ b/api/mcp/graphrag_init.py @@ -22,13 +22,9 @@ from __future__ import annotations import os -from typing import Tuple - -from graphrag_sdk import KnowledgeGraph, KnowledgeGraphModelConfig -from graphrag_sdk.models.litellm import LiteModel +from typing import Any, Tuple from api.graph import compose_graph_name -from api.llm import define_ontology from api.mcp.code_prompts import ( CYPHER_GEN_PROMPT, CYPHER_GEN_SYSTEM, @@ -37,25 +33,29 @@ ) -_CACHE: dict[Tuple[str, str], KnowledgeGraph] = {} +# Lazily imported to keep the MCP server starting even on graphrag-sdk 1.x, +# where ``KnowledgeGraph`` was removed. The ``ask`` tool still requires the +# 0.8 surface; structural tools work without it. +_CACHE: dict[Tuple[str, str], Any] = {} -def _make_model() -> LiteModel: - """Build the LiteModel from ``$MODEL_NAME`` (same default as api/llm.py).""" +def _make_model(): + """Build the LiteModel from ``$MODEL_NAME``.""" + from graphrag_sdk.models.litellm import LiteModel model_name = os.getenv("MODEL_NAME", "gemini/gemini-flash-lite-latest") return LiteModel(model_name) -def get_or_create_kg(project_name: str, branch: str = "_default") -> KnowledgeGraph: - """Return a cached :class:`KnowledgeGraph` for ``(project, branch)``. - - Two calls with the same ``(project, branch)`` are guaranteed to return - the **same** instance (identity preserved) so callers don't pay the - construction cost on every ``ask``. +def get_or_create_kg(project_name: str, branch: str = "_default"): + """Return a cached ``KnowledgeGraph`` for ``(project, branch)``. - The underlying graph name uses the T17 convention - ``code:{project}:{branch}`` so per-branch indexing works end-to-end. + Raises ``ImportError`` at call time if the installed graphrag-sdk does + not expose ``KnowledgeGraph`` (i.e. 1.x). This keeps the rest of the + MCP server functional under either SDK major. """ + from graphrag_sdk import KnowledgeGraph, KnowledgeGraphModelConfig # noqa: WPS433 + from api.llm import define_ontology # noqa: WPS433 + key = (project_name, branch) cached = _CACHE.get(key) if cached is not None: From 38d241137a5660475dedf59a5f8d24f074a29883 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 11:09:02 +0300 Subject: [PATCH 47/63] fix(bench): silence cgraph-mcp stderr so DEBUG logs don't bloat agent context Each cg-mcp bash invocation spawns a fresh cgraph-mcp server, whose DEBUG logs (analyzer init + MCP server.py registration + per-request dispatch) were being merged into the agent's tool-output buffer at ~1.8 kB per call. Across a 50-call trajectory that's ~90 kB of useless log noise replayed each turn, blowing token counts up to ~9x what the HTTP code_graph track produces. Route the spawned server's stderr to /dev/null via stdio_client's errlog kwarg. Verified end-to-end: pytest-6202 code_graph_mcp trajectory dropped from $6+ to $2.48. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_mcp_adapter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bench/agents/code_graph_mcp_adapter.py b/bench/agents/code_graph_mcp_adapter.py index 9a6347bd..0e1c2391 100644 --- a/bench/agents/code_graph_mcp_adapter.py +++ b/bench/agents/code_graph_mcp_adapter.py @@ -70,7 +70,12 @@ def _extract(result: Any) -> Any: async def _call_tool_async(name: str, arguments: dict[str, Any], timeout: float) -> Any: params = StdioServerParameters(command="cgraph-mcp", args=[], env=_env_for_mcp()) - async with stdio_client(params) as (read, write): + # Silence the server's stderr (analyzer + MCP DEBUG logs). When the + # bash shim is invoked by the agent, the server's stderr is merged + # into the agent's tool-output buffer, inflating context by ~1.8kB + # per call. The agent only needs the JSON-RPC result on stdout. + devnull = open(os.devnull, "w") + async with stdio_client(params, errlog=devnull) as (read, write): async with ClientSession(read, write) as session: await asyncio.wait_for(session.initialize(), timeout=timeout) result = await asyncio.wait_for( From bbb5d958024ae5e8ae3a7f1b58a2804a24964571 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 11:18:28 +0300 Subject: [PATCH 48/63] =?UTF-8?q?fix(bench):=20bump=20default=20cgraph-mcp?= =?UTF-8?q?=20timeout=2060s=20=E2=86=92=20300s=20for=20large=20repos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 of 10 calibration instances (sympy/django) hit TimeoutError during indexing at the 60s default. The sympy graphs alone have 24k+ nodes and 145k+ edges, which legitimately exceeds 60s. 300s matches the HTTP code_graph adapter's behaviour for large repos and removes the indexing-timeout failure mode without slowing happy-path calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_mcp_adapter.py | 2 +- bench/cli/cg_mcp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bench/agents/code_graph_mcp_adapter.py b/bench/agents/code_graph_mcp_adapter.py index 0e1c2391..4cfcc5fe 100644 --- a/bench/agents/code_graph_mcp_adapter.py +++ b/bench/agents/code_graph_mcp_adapter.py @@ -30,7 +30,7 @@ from mcp.client.stdio import stdio_client -DEFAULT_TIMEOUT_SEC = 60.0 +DEFAULT_TIMEOUT_SEC = 300.0 def _env_for_mcp() -> dict[str, str]: diff --git a/bench/cli/cg_mcp.py b/bench/cli/cg_mcp.py index 95c91390..31dc6234 100644 --- a/bench/cli/cg_mcp.py +++ b/bench/cli/cg_mcp.py @@ -47,7 +47,7 @@ def _print(obj: Any) -> None: def _timeout() -> float: try: - return float(os.getenv("CGRAPH_MCP_TIMEOUT_SEC", "60")) + return float(os.getenv("CGRAPH_MCP_TIMEOUT_SEC", "300")) except ValueError: return 60.0 From aa850d61e39de6e6410bbc0521e488f81e4e8b48 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 12:21:29 +0300 Subject: [PATCH 49/63] feat(bench): tool-availability precheck + per-trajectory tool-usage rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two safeguards against the 'silent fallback to bash' failure mode that made our Sonnet calibration headline numbers untrustworthy: 1. verify_tool_available(): before launching the agent in any tool config (lsp / code_graph / code_graph_mcp), exec the tool's --help in the same env the agent will see. If it fails (missing PATH, Python startup crash, etc.) the run aborts with outcome= 'tool_unavailable' instead of silently producing a bash-only trajectory that we'd later attribute to the tool. 2. compute_tool_usage(): for every trajectory, count how many bash commands actually invoked the configured tool (cg / cg-mcp / lsp). Surfaced as tool_usage_rate on TaskMetrics and as a new column in report.md. Sonnet calibration backfill revealed: code_graph median rate 12% (8 of 10 ⚠️) code_graph_mcp median rate 10% (10 of 10 ⚠️) lsp median rate 27% (7 of 10 ⚠️) So the agent abandoned the tool after a few attempts and ran 80-90% of bash commands as plain grep/sed/cat — meaning the '-30.5% MCP vs baseline' headline is mostly preamble effect, not tool effect. Reframes the experiment substantially. 3. Backfilled tool_usage_rate on all 40 existing Sonnet trajectories in mcp-t17/bench/cache/results.jsonl so future report renders show the column. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/metrics/__init__.py | 8 ++- bench/report/__init__.py | 13 ++++- bench/runners/mini_runner.py | 105 +++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/bench/metrics/__init__.py b/bench/metrics/__init__.py index 4427b55f..609d059c 100644 --- a/bench/metrics/__init__.py +++ b/bench/metrics/__init__.py @@ -39,9 +39,15 @@ class TaskMetrics: # tool-call sanity check tool_calls_total: int tool_calls_by_name: dict[str, int] = field(default_factory=dict) + # Tool-usage rate: fraction of bash commands that actually invoke the + # configured tool (cg / cg-mcp / lsp). Low rate = agent abandoned the + # tool and ran on plain bash. None for baseline (no tool expected). + tool_usage_rate: float | None = None + tool_usage_turns: int = 0 + tool_usage_total: int = 0 # outcome (set after scoring; None until then) - outcome: str | None = None # "resolved" | "failed" | "budget_exceeded" | "error" + outcome: str | None = None # "resolved" | "failed" | "budget_exceeded" | "error" | "tool_unavailable" patch: str | None = None wall_clock_sec: float | None = None diff --git a/bench/report/__init__.py b/bench/report/__init__.py index 4b304fde..5a264b9b 100644 --- a/bench/report/__init__.py +++ b/bench/report/__init__.py @@ -23,6 +23,7 @@ class ConfigSummary: n_resolved: int median_tokens: int p90_tokens: int + median_tool_usage: float | None = None # fraction in [0,1] or None for baseline @property def resolve_rate(self) -> float: @@ -77,6 +78,10 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: token_sums = [ (r["input_tokens"] + r["output_tokens"]) for r in best_by_task.values() ] + usage_rates = [ + r["tool_usage_rate"] for r in best_by_task.values() + if r.get("tool_usage_rate") is not None + ] n_resolved = sum(1 for r in best_by_task.values() if r.get("outcome") == "resolved") summaries.append( ConfigSummary( @@ -86,6 +91,7 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: n_resolved=n_resolved, median_tokens=int(statistics.median(token_sums)) if token_sums else 0, p90_tokens=_percentile(token_sums, 0.9), + median_tool_usage=statistics.median(usage_rates) if usage_rates else None, ) ) return summaries @@ -103,17 +109,18 @@ def render_markdown(summaries: list[ConfigSummary]) -> str: baseline_med = baseline.median_tokens if baseline else 0 lines.append(f"## {bench}\n") - lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline |") - lines.append("|---|---:|---:|---:|---:|---:|---:|") + lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | tool-usage rate |") + lines.append("|---|---:|---:|---:|---:|---:|---:|---:|") for s in sorted(group, key=lambda x: x.config): delta = "—" if baseline_med and s.config != "baseline": pct = (s.median_tokens - baseline_med) / baseline_med * 100 delta = f"{pct:+.1f}%" + usage = "—" if s.median_tool_usage is None else f"{s.median_tool_usage * 100:.0f}%" lines.append( f"| {s.config} | {s.n_tasks} | {s.n_resolved} | " f"{s.resolve_rate * 100:.1f}% | {s.median_tokens:,} | " - f"{s.p90_tokens:,} | {delta} |" + f"{s.p90_tokens:,} | {delta} | {usage} |" ) lines.append("") return "\n".join(lines) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 3081bd6e..16d224a0 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -425,6 +425,80 @@ def _capture_diff(repo_path: Path) -> str: return "" +def verify_tool_available(config: str, env: dict[str, str], cwd: Path) -> tuple[bool, str]: + """Smoke-test the agent's primary tool before launching the trajectory. + + Returns (ok, message). When the tool is missing or crashes at startup, + the agent will silently fall back to plain bash and we'd attribute its + cheaper trajectory to the "tool" — invalidating the experiment. This + precheck makes that failure mode loud. + """ + if config == "baseline": + return True, "baseline: no tool" + cmd_map = { + "lsp": ["lsp", "--help"], + "code_graph": ["cg", "--help"], + "code_graph_mcp": ["cg-mcp", "--help"], + } + cmd = cmd_map.get(config) + if cmd is None: + return True, f"no precheck for config {config!r}" + try: + res = subprocess.run( + cmd, cwd=str(cwd), env=env, + capture_output=True, text=True, timeout=15, + ) + except FileNotFoundError as e: + return False, f"{cmd[0]} not on PATH: {e}" + except subprocess.TimeoutExpired: + return False, f"{cmd[0]} --help timed out (>15s)" + if res.returncode != 0: + tail = (res.stderr or res.stdout)[-300:] + return False, f"{cmd[0]} --help returncode={res.returncode}: {tail}" + return True, f"{cmd[0]} ok" + + +TOOL_KEYWORDS = { + "baseline": (), + "lsp": ("lsp",), + "code_graph": ("cg ",), + "code_graph_mcp": ("cg-mcp",), +} + + +def compute_tool_usage(messages: list[dict[str, Any]], config: str) -> dict[str, Any]: + """Count assistant bash commands that actually invoke the configured tool. + + Returns {turns, tool_turns, rate}. A low rate (<0.5) on a tool config + means the agent stopped using its tool — usually because the tool + crashed or was unhelpful — and the trajectory is effectively a baseline + run with extra preamble. Surface this in the report so we don't + misattribute baseline-like behaviour to the tool. + """ + kws = TOOL_KEYWORDS.get(config, ()) + if not kws: + return {"turns": 0, "tool_turns": 0, "rate": None} + turns = 0 + tool_turns = 0 + for m in messages: + if m.get("role") != "assistant": + continue + # mini-swe-agent v2 puts the bash command in tool_calls[*].function.arguments + tcs = m.get("tool_calls") or [] + for tc in tcs: + fn = tc.get("function") or {} + if fn.get("name") != "bash": + continue + args = fn.get("arguments") or "" + if isinstance(args, dict): + args = args.get("command", "") + turns += 1 + if any(kw in args for kw in kws): + tool_turns += 1 + rate = tool_turns / turns if turns else None + return {"turns": turns, "tool_turns": tool_turns, "rate": rate} + + def run_task( task: Task, config: str, @@ -454,6 +528,28 @@ def run_task( from minisweagent.environments.local import LocalEnvironment env_vars = config_env(config, task.repo_path) + + # PRECHECK: verify the tool actually launches before spending model $$. + # If the tool crashes (e.g. cg shim hits "Bad file descriptor" on Python + # init), the agent will silently fall back to bash for the entire + # trajectory and we'd attribute its behaviour to the tool. Hard-fail here + # so the issue is visible. + if not dry_run: + tool_ok, tool_msg = verify_tool_available(config, env_vars, task.repo_path) + if not tool_ok: + from bench.metrics import TaskMetrics + return { + "metrics": TaskMetrics( + benchmark=benchmark, task_id=task.task_id, config=config, + run_idx=run_idx, outcome="tool_unavailable", + wall_clock_sec=0.0, + ), + "trajectory": {"info": {"tool_precheck": tool_msg}}, + "exit_status": "error", + "exit_reason": f"tool precheck failed for {config}: {tool_msg}", + "diff": "", + } + env = LocalEnvironment(cwd=str(task.repo_path), env=env_vars, timeout=120) preamble = load_preamble(config) @@ -491,6 +587,12 @@ def run_task( # didn't embed it. trajectory.setdefault("info", {})["submission"] = diff + # Tool-usage instrumentation: surface trajectories where the agent + # silently abandoned the configured tool. Stored on the trajectory + # and on the metrics row so report.py can flag low-usage runs. + tool_usage = compute_tool_usage(trajectory.get("messages", []), config) + trajectory["info"]["tool_usage"] = tool_usage + from bench.metrics import TaskMetrics, task_metrics_from_trajectory metrics: TaskMetrics = task_metrics_from_trajectory( @@ -501,6 +603,9 @@ def run_task( run_idx=run_idx, wall_clock_sec=round(wall, 3), ) + metrics.tool_usage_rate = tool_usage["rate"] + metrics.tool_usage_turns = tool_usage["tool_turns"] + metrics.tool_usage_total = tool_usage["turns"] if exit_status == "error": metrics.outcome = "error" From 4a6956e15a2da4c65a5588db8fd8943027a69a10 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 12:32:34 +0300 Subject: [PATCH 50/63] fix(bench): defensive stdin redirect + anti-fallback preamble rules Two fixes addressing the 10-27% tool-usage rate observed in the Sonnet calibration: 1. cg / cg-mcp / lsp shims: redirect stdin from /dev/null on exec. mini-swe-agent's LocalEnvironment runs commands via subprocess.run(shell=True) without specifying stdin. When the runner is nohup-detached or run in a context with a closed FD 0, Python crashes at interpreter startup with init_sys_streams: Bad file descriptor before our argparse code runs. The Opus probe on pytest-6202 showed the first cg call crashing this way, after which the agent wrapped subsequent calls in '|| echo failed' and ran the rest of the trajectory on plain bash. Defense-in-depth only; harmless when FD 0 is already valid. 2. code_graph / code_graph_mcp / lsp preambles: add explicit rules forbidding silent fallback to grep/find. The agent must state tool failure before using a textual search alternative. This gives us a chance to (a) actually diagnose tool failures from trajectories instead of silently scoring bash trajectories as tool wins, and (b) raise tool-usage rates closer to a regime where the tool can plausibly affect outcomes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/cli/cg | 10 +++++++++- bench/cli/cg-mcp | 6 +++++- bench/cli/lsp | 2 +- bench/tools/code_graph/system_preamble.md | 15 +++++++++++++++ bench/tools/code_graph_mcp/system_preamble.md | 10 ++++++++++ bench/tools/lsp/system_preamble.md | 13 +++++++++++++ 6 files changed, 53 insertions(+), 3 deletions(-) diff --git a/bench/cli/cg b/bench/cli/cg index b9fd108c..5c4ef685 100755 --- a/bench/cli/cg +++ b/bench/cli/cg @@ -3,4 +3,12 @@ # bench/cli on PATH so the agent can invoke `cg ...` directly. # BENCH_PYTHON is set by the runner to the venv's python; falls back # to system python3 if missing (won't have deps; will fail loudly). -exec "${BENCH_PYTHON:-python3}" -m bench.cli.cg "$@" +# +# stdin is redirected from /dev/null defensively: mini-swe-agent's +# LocalEnvironment runs commands via subprocess.run(shell=True) without +# specifying stdin. When the runner is nohup-detached, the inherited +# FD 0 can be closed/invalid, causing Python to crash at startup with +# `init_sys_streams: Bad file descriptor` before our code ever runs. +# Closing/redirecting stdin to /dev/null gives Python a valid FD. +# -I would also work but we want sys.path manipulation if needed. +exec "${BENCH_PYTHON:-python3}" -m bench.cli.cg "$@" Date: Thu, 28 May 2026 12:38:46 +0300 Subject: [PATCH 51/63] fix(bench): grade via official swebench Docker harness; deprecate broken pytest verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old verify_instance ran modern pytest 8 from the bench-combined venv against legacy SWE-bench worktrees. Old codebases like pytest-6202 use config keys (rsyncdirs) removed in modern pytest, producing `INTERNALERROR: Unknown config option` at collection time — 0 tests collected, returncode!=0, every trajectory graded 'failed' regardless of patch correctness. The 1-task Opus probe proved this: 3 of 4 configs produced the exact gold patch, all 4 graded 'failed' for the same config error. Replacement: verify_with_swebench_harness(inst, patch, ...) writes a predictions.jsonl in the format the official harness expects and calls swebench.harness.run_evaluation.main, which spins up per-instance Docker images with the correct Python + dependency set and runs the real FAIL_TO_PASS + PASS_TO_PASS selection. The agent's patch comes from trajectory.info.submission (already populated by the runner). When Docker is absent the result is reported as outcome= 'verifier_unavailable' rather than silently graded 'failed' — strictly more honest, and lets the report distinguish 'agent failed' from 'we don't know'. The old verify_instance is kept as a deprecation shim so any leftover caller fails loud. Also adds: - mini_runner --skip-verify / --verify-timeout flags - bench.cli.regrade for retroactively grading existing trajectories without re-running the agent (saves the tokens spent on the 40 Sonnet calibration once Docker is wired up) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/cli/regrade.py | 126 +++++++++++++ bench/datasets/swe_bench.py | 161 ++++++++++++++-- bench/runners/mini_runner.py | 32 +++- uv.lock | 353 ++++++++++++++++++++++++++++------- 4 files changed, 584 insertions(+), 88 deletions(-) create mode 100644 bench/cli/regrade.py diff --git a/bench/cli/regrade.py b/bench/cli/regrade.py new file mode 100644 index 00000000..2df68c0a --- /dev/null +++ b/bench/cli/regrade.py @@ -0,0 +1,126 @@ +"""Retroactively grade existing trajectories via the SWE-bench Docker harness. + +Usage: + python -m bench.cli.regrade \ + --trajectories bench/cache/trajectories \ + --results bench/cache/results.jsonl \ + [--instance-id pytest-dev__pytest-6202] \ + [--config code_graph_mcp] + +For each trajectory file found, extracts the agent's submitted patch from +`info.submission` (and falls back to scanning messages), invokes the +official swebench harness via `bench.datasets.swe_bench.verify_with_swebench_harness`, +and updates the matching row in `results.jsonl` with `outcome=resolved|failed|verifier_unavailable` +plus a `verify_summary`. + +Requires Docker on the host. Without it every row will end up flagged +`verifier_unavailable` (which is still strictly more honest than the +old verifier's silent `failed`). +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from bench.datasets.swe_bench import ( + SweBenchInstance, + load_instances, + verify_with_swebench_harness, + _docker_available, +) + + +def _patch_from_trajectory(path: Path) -> str: + try: + data = json.loads(path.read_text()) + except Exception: + return "" + info = data.get("info", {}) + sub = info.get("submission") or info.get("patch") or "" + if sub: + return sub + # Fallback: look for a `git diff` in trailing assistant messages. + msgs = data.get("messages", []) + for m in reversed(msgs): + content = m.get("content") or "" + if isinstance(content, str) and content.startswith("diff --git"): + return content + return "" + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser() + p.add_argument("--trajectories", type=Path, required=True) + p.add_argument("--results", type=Path, required=True) + p.add_argument("--instance-id", default=None) + p.add_argument("--config", default=None) + p.add_argument("--namespace", default="swebench", + help="Docker image namespace. 'swebench' = prebuilt; " + "None = local build.") + p.add_argument("--timeout", type=int, default=1800) + args = p.parse_args(argv) + + if not _docker_available(): + print("[regrade] docker not available — every row will be marked " + "verifier_unavailable") + + insts_by_id = {i.instance_id: i for i in load_instances()} + + # Index existing results by (task_id, config) so we can patch them. + rows = [] + for line in args.results.read_text().splitlines(): + if line.strip(): + rows.append(json.loads(line)) + index = {(r["task_id"], r["config"]): r for r in rows} + + updated = 0 + for traj_path in sorted(args.trajectories.glob("*.json")): + stem = traj_path.stem # "__" + if "__" not in stem: + continue + instance_id, _, cfg = stem.rpartition("__") + if args.instance_id and instance_id != args.instance_id: + continue + if args.config and cfg != args.config: + continue + inst = insts_by_id.get(instance_id) + if not inst: + print(f"[regrade] {stem}: not in dataset, skip") + continue + patch = _patch_from_trajectory(traj_path) + if not patch.strip(): + print(f"[regrade] {stem}: empty patch") + row = index.get((instance_id, cfg)) + if row: + row["outcome"] = "failed" + row["verify_summary"] = "empty patch" + updated += 1 + continue + resolved, summary = verify_with_swebench_harness( + inst, patch, + run_id=f"regrade-{instance_id}-{cfg}", + namespace=(None if args.namespace.lower() == "none" else args.namespace), + timeout=args.timeout, + ) + if resolved is None: + outcome = "verifier_unavailable" + else: + outcome = "resolved" if resolved else "failed" + print(f"[regrade] {stem}: {outcome} — {summary[:120]}") + row = index.get((instance_id, cfg)) + if row: + row["outcome"] = outcome + row["verify_summary"] = summary[-300:] + updated += 1 + + with args.results.open("w") as f: + for r in rows: + f.write(json.dumps(r) + "\n") + print(f"[regrade] updated {updated} rows in {args.results}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/bench/datasets/swe_bench.py b/bench/datasets/swe_bench.py index 6f89373a..6f73cb73 100644 --- a/bench/datasets/swe_bench.py +++ b/bench/datasets/swe_bench.py @@ -215,8 +215,143 @@ def instance_to_task(inst: SweBenchInstance, repo_path: Path) -> Task: # --------------------------------------------------------------------------- -# Verification (approximate — official harness needs Docker) +# Verification — official SWE-bench harness path # --------------------------------------------------------------------------- +# +# The original verify_instance implementation ran modern pytest from the +# bench venv against the SWE-bench worktree's legacy code, which collected +# zero tests on most instances (e.g. pytest-6202 INTERNALERRORs on removed +# config keys like `rsyncdirs`). Every trajectory was graded `failed` +# regardless of patch correctness, invalidating the Sonnet calibration's +# 0/10 resolve rate. +# +# The replacement defers to the official swebench harness, which builds / +# pulls per-instance Docker images with the right Python + dependencies +# and runs FAIL_TO_PASS + PASS_TO_PASS exactly the way the leaderboard +# does. Requires Docker on the host; when Docker is absent we return a +# `verifier_unavailable` outcome so we never silently grade wrongly again. + + +def _docker_available() -> bool: + """Cheap probe — does `docker info` succeed?""" + try: + r = subprocess.run( + ["docker", "info"], + capture_output=True, + text=True, + timeout=5, + ) + return r.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def verify_with_swebench_harness( + inst: SweBenchInstance, + patch: str, + *, + run_id: str | None = None, + namespace: str | None = "swebench", + timeout: int = 1800, + report_dir: Path | None = None, +) -> tuple[bool | None, str]: + """Grade a single (instance, patch) via the official swebench harness. + + Returns (resolved, summary): + - (True, "..." ) — FAIL_TO_PASS flipped + PASS_TO_PASS held + - (False, "...") — patch did not resolve + - (None, "verifier_unavailable: ") — could not grade (no + Docker, harness exception). Caller should record outcome= + `verifier_unavailable` rather than `failed`. + + `namespace="swebench"` pulls prebuilt images from Docker Hub (the + `swebench/sweb.eval.x86_64.` family) and avoids the 30+ + minute per-instance build step. Pass `namespace=None` to force a + local build instead. + """ + if not _docker_available(): + return None, "verifier_unavailable: docker not available on host" + + if not patch.strip(): + return False, "empty patch" + + run_id = run_id or f"code-graph-bench-{inst.instance_id}" + report_dir = report_dir or (DEFAULT_CACHE_ROOT / "verify" / run_id) + report_dir.mkdir(parents=True, exist_ok=True) + + # predictions.jsonl in the format the harness expects + model_tag = "code-graph-bench" + pred_path = report_dir / "predictions.jsonl" + with pred_path.open("w") as f: + f.write(json.dumps({ + "instance_id": inst.instance_id, + "model_name_or_path": model_tag, + "model_patch": patch, + }) + "\n") + + try: + from swebench.harness.run_evaluation import main as run_eval + except Exception as e: # pragma: no cover — bench extra missing + return None, f"verifier_unavailable: swebench import failed: {e}" + + try: + run_eval( + dataset_name=DATASET_NAME, + split="test", + instance_ids=[inst.instance_id], + predictions_path=str(pred_path), + max_workers=1, + force_rebuild=False, + cache_level="env", + clean=False, + open_file_limit=4096, + run_id=run_id, + timeout=timeout, + namespace=namespace, + rewrite_reports=False, + modal=False, + report_dir=str(report_dir), + ) + except Exception as e: + return None, f"verifier_unavailable: harness raised {type(e).__name__}: {e}" + + # The harness writes per-instance reports under + # logs/run_evaluation////report.json + # but the path is CWD-relative. Find the resulting report. + candidates = list(Path.cwd().glob( + f"logs/run_evaluation/{run_id}/{model_tag}/{inst.instance_id}/report.json" + )) + if not candidates: + # Fallback: the harness writes a top-level run report too. + top = list(report_dir.glob(f"*.{run_id}.json")) + if top: + try: + data = json.loads(top[0].read_text()) + resolved_ids = set(data.get("resolved_ids", [])) + ok = inst.instance_id in resolved_ids + return ok, f"top-level report: resolved={ok}" + except Exception: + pass + return None, "verifier_unavailable: no per-instance report produced" + + try: + data = json.loads(candidates[0].read_text()) + # The per-instance report nests under the instance_id. + inst_data = data.get(inst.instance_id, data) + resolved = bool(inst_data.get("resolved")) + tests_status = inst_data.get("tests_status", {}) + f2p = tests_status.get("FAIL_TO_PASS", {}) + p2p = tests_status.get("PASS_TO_PASS", {}) + summary = ( + f"resolved={resolved} " + f"F2P: success={len(f2p.get('success', []))} " + f"failure={len(f2p.get('failure', []))} " + f"P2P: success={len(p2p.get('success', []))} " + f"failure={len(p2p.get('failure', []))}" + ) + return resolved, summary + except Exception as e: + return None, f"verifier_unavailable: report parse failed: {e}" def verify_instance( @@ -225,20 +360,14 @@ def verify_instance( *, python: str | None = None, ) -> tuple[bool, str]: - """Run FAIL_TO_PASS + PASS_TO_PASS tests against the patched repo. + """DEPRECATED — runs modern pytest against legacy repos and returns + bogus results. Kept as a thin shim that fails loud so any caller + still using it gets a clear message instead of a silent wrong grade. - Returns (passed, summary). Best-effort: many SWE-bench repos need - bespoke conda envs we don't build here. If pytest itself fails to - collect, returns (False, "") so the runner records `failed` - and we know to investigate. + Real verification goes through `verify_with_swebench_harness(inst, + patch)`, which uses the official swebench Docker harness. """ - py = python or os.environ.get("BENCH_REPO_PYTHON") or "python" - test_ids = list(inst.fail_to_pass) + list(inst.pass_to_pass) - if not test_ids: - return False, "no test ids in dataset row" - - cmd = [py, "-m", "pytest", "-q", "--no-header", "-p", "no:cacheprovider", *test_ids] - res = subprocess.run(cmd, cwd=str(repo_path), capture_output=True, text=True) - ok = res.returncode == 0 - summary = res.stdout[-500:] + res.stderr[-500:] - return ok, summary + return False, ( + "verify_instance is deprecated; use verify_with_swebench_harness " + "with the agent's submitted patch and a Docker-enabled host." + ) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 16d224a0..b27321a2 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -800,6 +800,13 @@ def main(argv: list[str] | None = None) -> int: p.add_argument("--step-limit", type=int, default=50) p.add_argument("--cost-limit", type=float, default=3.0) p.add_argument("--wall-time", type=int, default=1200) + p.add_argument("--skip-verify", action="store_true", + help="Skip SWE-bench Docker verification; record " + "outcome=verify_skipped. Useful on hosts without " + "Docker for token-cost / tool-usage measurement runs.") + p.add_argument("--verify-timeout", type=int, default=1800, + help="Per-instance verification timeout in seconds " + "passed to swebench.harness (default 1800).") args = p.parse_args(argv) configs = args.config or list(VALID_CONFIGS) @@ -811,7 +818,7 @@ def main(argv: list[str] | None = None) -> int: if args.swe_bench: from bench.datasets.swe_bench import ( load_instances, sample_instances, prepare_worktree, - instance_to_task, verify_instance, + instance_to_task, verify_with_swebench_harness, ) from bench.metrics import append_jsonl @@ -860,10 +867,25 @@ def main(argv: list[str] | None = None) -> int: defer_jsonl=True, ) rows.extend(cfg_rows) - ok, summary = verify_instance(inst, cfg_wt) - cfg_rows[-1]["metrics"].outcome = "resolved" if ok else "failed" - if not ok: - cfg_rows[-1]["verify_summary"] = summary[-200:] + # Official SWE-bench harness verification. The agent's + # patch is on the trajectory metrics; pass it to the + # Docker-backed harness. When Docker is missing the + # outcome is recorded as `verifier_unavailable` rather + # than silently graded `failed`. + patch = (cfg_rows[-1]["metrics"].patch or "") if cfg_rows else "" + if args.skip_verify: + cfg_rows[-1]["metrics"].outcome = "verify_skipped" + else: + resolved, summary = verify_with_swebench_harness( + inst, patch, timeout=args.verify_timeout, + ) + if resolved is None: + cfg_rows[-1]["metrics"].outcome = "verifier_unavailable" + else: + cfg_rows[-1]["metrics"].outcome = ( + "resolved" if resolved else "failed" + ) + cfg_rows[-1]["verify_summary"] = summary[-300:] append_jsonl(args.results, cfg_rows[-1]["metrics"]) else: with tempfile.TemporaryDirectory() as td: diff --git a/uv.lock b/uv.lock index 8864a789..dff8efbb 100644 --- a/uv.lock +++ b/uv.lock @@ -297,64 +297,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] -[[package]] -name = "code-graph-backend" -version = "0.4.2" -source = { virtual = "." } -dependencies = [ - { name = "falkordb" }, - { name = "fastapi" }, - { name = "graphrag-sdk" }, - { name = "javatools" }, - { name = "multilspy" }, - { name = "pygit2" }, - { name = "python-dotenv" }, - { name = "tree-sitter" }, - { name = "tree-sitter-c" }, - { name = "tree-sitter-c-sharp" }, - { name = "tree-sitter-java" }, - { name = "tree-sitter-python" }, - { name = "uvicorn", extra = ["standard"] }, - { name = "validators" }, -] - -[package.optional-dependencies] -bench = [ - { name = "datasets" }, - { name = "mini-swe-agent" }, - { name = "swebench" }, -] -test = [ - { name = "httpx" }, - { name = "pytest" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "datasets", marker = "extra == 'bench'", specifier = ">=4.8.5" }, - { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, - { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, - { name = "graphrag-sdk", specifier = ">=1.1.1,<2.0.0" }, - { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, - { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, - { name = "mini-swe-agent", marker = "extra == 'bench'", specifier = ">=1.0.0" }, - { name = "multilspy", git = "https://github.com/AviAvni/multilspy.git?rev=python-init-params" }, - { name = "pygit2", specifier = ">=1.17.0,<2.0.0" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, - { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, - { name = "ruff", marker = "extra == 'test'", specifier = ">=0.11.0,<1.0.0" }, - { name = "swebench", marker = "extra == 'bench'", specifier = ">=4.0" }, - { name = "tree-sitter", specifier = ">=0.25.2,<0.26.0" }, - { name = "tree-sitter-c", specifier = ">=0.24.1,<0.25.0" }, - { name = "tree-sitter-c-sharp", specifier = ">=0.23.1,<0.24.0" }, - { name = "tree-sitter-java", specifier = ">=0.23.5,<0.24.0" }, - { name = "tree-sitter-python", specifier = ">=0.25.0,<0.26.0" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, - { name = "validators", specifier = ">=0.35.0,<0.36.0" }, -] -provides-extras = ["test", "bench"] - [[package]] name = "colorama" version = "0.4.6" @@ -364,6 +306,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + [[package]] name = "ct3" version = "3.4.0.post5" @@ -534,6 +515,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8b/59ec60885abd3b6b2b3a1e5917627c3cae656b4cff7f847c5217ec3dc952/falkordb-1.6.0-py3-none-any.whl", hash = "sha256:0f190e9d6104595fd51ece4f1e7b5d49d62cfee346d94151d7986a138fd90d89", size = 37378, upload-time = "2026-02-21T06:36:17.769Z" }, ] +[[package]] +name = "falkordb-code-graph" +version = "0.4.2" +source = { editable = "." } +dependencies = [ + { name = "falkordb" }, + { name = "falkordb-multilspy" }, + { name = "fastapi" }, + { name = "graphrag-sdk" }, + { name = "javatools" }, + { name = "mcp" }, + { name = "pygit2" }, + { name = "python-dotenv" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c" }, + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-java" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-kotlin" }, + { name = "tree-sitter-python" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "validators" }, +] + +[package.optional-dependencies] +bench = [ + { name = "datasets" }, + { name = "mini-swe-agent" }, + { name = "swebench" }, +] +test = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-anyio" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", marker = "extra == 'test'", specifier = ">=4.0,<5.0" }, + { name = "datasets", marker = "extra == 'bench'", specifier = ">=4.8.5" }, + { name = "falkordb", specifier = ">=1.1.3,<2.0.0" }, + { name = "falkordb-multilspy", specifier = ">=0.1.0,<1.0.0" }, + { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, + { name = "graphrag-sdk", specifier = ">=1.1.1,<2.0.0" }, + { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, + { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, + { name = "mcp", specifier = ">=1.0.0,<2.0.0" }, + { name = "mini-swe-agent", marker = "extra == 'bench'", specifier = ">=1.0.0" }, + { name = "pygit2", specifier = ">=1.17.0,<2.0.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, + { name = "ruff", marker = "extra == 'test'", specifier = ">=0.11.0,<1.0.0" }, + { name = "swebench", marker = "extra == 'bench'", specifier = ">=4.0" }, + { name = "tree-sitter", specifier = ">=0.25.2,<0.26.0" }, + { name = "tree-sitter-c", specifier = ">=0.24.1,<0.25.0" }, + { name = "tree-sitter-c-sharp", specifier = ">=0.23.1,<0.24.0" }, + { name = "tree-sitter-java", specifier = ">=0.23.5,<0.24.0" }, + { name = "tree-sitter-javascript", specifier = ">=0.23.0,<0.26.0" }, + { name = "tree-sitter-kotlin", specifier = ">=1.1.0,<2.0.0" }, + { name = "tree-sitter-python", specifier = ">=0.25.0,<0.26.0" }, + { name = "typer", specifier = ">=0.24.0,<1.0.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1.0.0" }, + { name = "validators", specifier = ">=0.35.0,<0.36.0" }, +] +provides-extras = ["test", "bench"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-anyio", specifier = ">=0.0.0" }, + { name = "pyyaml", specifier = ">=6.0.3" }, +] + +[[package]] +name = "falkordb-multilspy" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jedi-language-server" }, + { name = "psutil" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/4e/50ac939db4243dff2cf3ddd94cd75233b30f06470141cca09ed0d70a2f6c/falkordb_multilspy-0.1.0.tar.gz", hash = "sha256:779117d1b801322e30dc593d03da5eeda1d848209fc47d33534d3c9422a6f255", size = 115180, upload-time = "2026-03-23T14:29:34.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/15/97032c229a031b29795c2d0a74646bc24b1bdbb71930c44d1d37ef9d98c6/falkordb_multilspy-0.1.0-py3-none-any.whl", hash = "sha256:3429f11a83c4fbf06c2b8f0078b8a257e0870da9d50ebd964c6de2ad33164f57", size = 129555, upload-time = "2026-03-23T14:29:32.936Z" }, +] + [[package]] name = "fastapi" version = "0.135.1" @@ -876,6 +954,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "huggingface-hub" version = "1.6.0" @@ -1174,6 +1261,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mcp" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.6.1" @@ -1316,15 +1428,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "multilspy" -version = "0.0.11" -source = { git = "https://github.com/AviAvni/multilspy.git?rev=python-init-params#4e8c6601c55173cddf862b9eb6cf1e9343394b83" } -dependencies = [ - { name = "jedi-language-server" }, - { name = "requests" }, -] - [[package]] name = "multiprocess" version = "0.70.19" @@ -1770,6 +1873,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pyarrow" version = "24.0.0" @@ -1866,6 +1991,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + [[package]] name = "pygit2" version = "1.19.1" @@ -1919,6 +2058,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -1935,6 +2088,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] +[[package]] +name = "pytest-anyio" +version = "0.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/44/a02e5877a671b0940f21a7a0d9704c22097b123ed5cdbcca9cab39f17acc/pytest-anyio-0.0.0.tar.gz", hash = "sha256:b41234e9e9ad7ea1dbfefcc1d6891b23d5ef7c9f07ccf804c13a9cc338571fd3", size = 1560, upload-time = "2021-06-29T22:57:30.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/25/bd6493ae85d0a281b6a0f248d0fdb1d9aa2b31f18bcd4a8800cf397d8209/pytest_anyio-0.0.0-py2.py3-none-any.whl", hash = "sha256:dc8b5c4741cb16ff90be37fddd585ca943ed12bbeb563de7ace6cd94441d8746", size = 1999, upload-time = "2021-06-29T22:57:29.158Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1969,6 +2135,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2343,6 +2518,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -2642,6 +2830,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/57/5bab54d23179350356515526fff3cc0f3ac23bfbc1a1d518a15978d4880e/tree_sitter_java-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:402efe136104c5603b429dc26c7e75ae14faaca54cfd319ecc41c8f2534750f4", size = 59059, upload-time = "2024-12-21T18:24:24.934Z" }, ] +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-kotlin" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/bb/bdab3665eeca21246130eec79c76e42456cfa72d59606266ecdbf37f9a96/tree_sitter_kotlin-1.1.0.tar.gz", hash = "sha256:322a35bdae75e25ae64dae6027be609c5422fab282084117816c4ebcda6168da", size = 1095728, upload-time = "2025-01-09T19:02:18.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/a5/ce5a2ba7b97db8d90c89516674f5c46e2d41503e00dd743ba7aad4661097/tree_sitter_kotlin-1.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6cca5ef06d090e8494ac1d9f0aac71ed32207d412766b5df7da00d94334181a2", size = 312883, upload-time = "2025-01-09T19:02:02.931Z" }, + { url = "https://files.pythonhosted.org/packages/7d/20/66105b6e94d062440955d374e64d030c3173cf4f592f6a6a3c426b3c94d0/tree_sitter_kotlin-1.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:910b41a580dae00d319e555075f3886a41386d1067931b14c7de504eeae3ae2a", size = 337016, upload-time = "2025-01-09T19:02:04.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/e1ef38fe412fa9851403fc75a653f2b69bbe1e11e2e7faf219631ebe7e4a/tree_sitter_kotlin-1.1.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:906e5444ebb01db439cb3ad65913598a4ea957b0e068aa973265926a17eb00e0", size = 359927, upload-time = "2025-01-09T19:02:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/65/bd/0f3aac45eb88b6b3173ac9c23bc41d8865943cbbe1caaafc001cd1b73c90/tree_sitter_kotlin-1.1.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92afe24b634cf914c5812af0f5c53184b1c18bdf6ee5505c83afac81f6bf6c", size = 339269, upload-time = "2025-01-09T19:02:08.644Z" }, + { url = "https://files.pythonhosted.org/packages/08/dc/4944abf3a8bc630262e93e0857bd7044d521995c1f6af50650e4fe1fdde0/tree_sitter_kotlin-1.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5960034a5c5bcc7ccb21dc7a29e4267ac4f0ef37884f39d75695eac7f004deff", size = 328921, upload-time = "2025-01-09T19:02:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/c9/5cca0a44db41224f7f10992450af17ff432c1a336852efb312246d5705e5/tree_sitter_kotlin-1.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:d4d3f330f515ba8b91da04a5335eb9ff3ce071c7b7855958912f2560f6e14976", size = 315933, upload-time = "2025-01-09T19:02:12.637Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b9/12fa97f63d2b7517c6f5d16938f0c5bfe84d925c652c75ff1c5e29bf6a44/tree_sitter_kotlin-1.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:e030f127a7d07952907adb9070248bd42fb86dc76fd92744727551b50e131ee7", size = 310414, upload-time = "2025-01-09T19:02:16.23Z" }, +] + [[package]] name = "tree-sitter-python" version = "0.25.0" From bfdf60da4faaf45a3ef67397cd70d4ea9766a85d Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 14:19:43 +0300 Subject: [PATCH 52/63] chore(bench): gitignore swebench harness output (regrade reports + logs) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index b7476d0a..49aaee68 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,8 @@ htmlcov/ pytest_cache/ *.log repositories/ + +# bench: SWE-bench harness output (regrade reports + per-instance logs) +bench/cache/verify/ +logs/run_evaluation/ +code-graph-bench.*.json From 7ab59f45c1af6752df99217885c6c9dbcb60775f Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 15:20:10 +0300 Subject: [PATCH 53/63] bench: add fallback_rate metric (passive grep/find tracking) Track the share of bash commands on each track that are plain text search (grep/rg/find/ack/ag) rather than the configured tool. Surfaced alongside tool_usage_rate so we can distinguish 'tool answered the question and the rest is normal bash' from 'agent silently abandoned the tool and reverted to grep'. Sonnet 4.5 n=10 headline now shows: - baseline 25% fallback - code_graph 10% (cut 60%) - code_graph_mcp 8% (cut 68%) - lsp 4% (cut 84%) Backfilled all 40 Sonnet trajectories and the 15 Opus trajectories currently on disk; harness writes the metric forward. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/metrics/__init__.py | 5 ++++ bench/report/__init__.py | 13 +++++++--- bench/runners/mini_runner.py | 48 +++++++++++++++++++++++++++--------- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/bench/metrics/__init__.py b/bench/metrics/__init__.py index 609d059c..ac32421a 100644 --- a/bench/metrics/__init__.py +++ b/bench/metrics/__init__.py @@ -45,6 +45,11 @@ class TaskMetrics: tool_usage_rate: float | None = None tool_usage_turns: int = 0 tool_usage_total: int = 0 + # Fallback rate: fraction of bash commands that are plain text search + # (grep / rg / find / ack / ag) instead of the configured tool. + # Always populated (incl. baseline as a reference point). + fallback_rate: float | None = None + fallback_turns: int = 0 # outcome (set after scoring; None until then) outcome: str | None = None # "resolved" | "failed" | "budget_exceeded" | "error" | "tool_unavailable" diff --git a/bench/report/__init__.py b/bench/report/__init__.py index 5a264b9b..f3c407b9 100644 --- a/bench/report/__init__.py +++ b/bench/report/__init__.py @@ -24,6 +24,7 @@ class ConfigSummary: median_tokens: int p90_tokens: int median_tool_usage: float | None = None # fraction in [0,1] or None for baseline + median_fallback: float | None = None # fraction in [0,1] of bash cmds that were grep/find/rg @property def resolve_rate(self) -> float: @@ -82,6 +83,10 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: r["tool_usage_rate"] for r in best_by_task.values() if r.get("tool_usage_rate") is not None ] + fallback_rates = [ + r["fallback_rate"] for r in best_by_task.values() + if r.get("fallback_rate") is not None + ] n_resolved = sum(1 for r in best_by_task.values() if r.get("outcome") == "resolved") summaries.append( ConfigSummary( @@ -92,6 +97,7 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: median_tokens=int(statistics.median(token_sums)) if token_sums else 0, p90_tokens=_percentile(token_sums, 0.9), median_tool_usage=statistics.median(usage_rates) if usage_rates else None, + median_fallback=statistics.median(fallback_rates) if fallback_rates else None, ) ) return summaries @@ -109,18 +115,19 @@ def render_markdown(summaries: list[ConfigSummary]) -> str: baseline_med = baseline.median_tokens if baseline else 0 lines.append(f"## {bench}\n") - lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | tool-usage rate |") - lines.append("|---|---:|---:|---:|---:|---:|---:|---:|") + lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | tool-usage rate | fallback rate |") + lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|") for s in sorted(group, key=lambda x: x.config): delta = "—" if baseline_med and s.config != "baseline": pct = (s.median_tokens - baseline_med) / baseline_med * 100 delta = f"{pct:+.1f}%" usage = "—" if s.median_tool_usage is None else f"{s.median_tool_usage * 100:.0f}%" + fb = "—" if s.median_fallback is None else f"{s.median_fallback * 100:.0f}%" lines.append( f"| {s.config} | {s.n_tasks} | {s.n_resolved} | " f"{s.resolve_rate * 100:.1f}% | {s.median_tokens:,} | " - f"{s.p90_tokens:,} | {delta} | {usage} |" + f"{s.p90_tokens:,} | {delta} | {usage} | {fb} |" ) lines.append("") return "\n".join(lines) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index b27321a2..ac2d3d44 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -33,6 +33,7 @@ import argparse import json import os +import re import subprocess import sys import time @@ -465,21 +466,31 @@ def verify_tool_available(config: str, env: dict[str, str], cwd: Path) -> tuple[ "code_graph_mcp": ("cg-mcp",), } +# Bash search commands the agent might fall back to instead of using the +# configured code-navigation tool. Matched as whole tokens against the +# bash command string. Tracked passively as a "fallback_rate" metric so +# we can quantify how often each tool track silently degrades to grep. +_FALLBACK_RE = re.compile(r"(?:^|[\s;&|`(])(grep|rg|find|ack|ag)(?:\s|$)") -def compute_tool_usage(messages: list[dict[str, Any]], config: str) -> dict[str, Any]: - """Count assistant bash commands that actually invoke the configured tool. - Returns {turns, tool_turns, rate}. A low rate (<0.5) on a tool config - means the agent stopped using its tool — usually because the tool - crashed or was unhelpful — and the trajectory is effectively a baseline - run with extra preamble. Surface this in the report so we don't - misattribute baseline-like behaviour to the tool. +def compute_tool_usage(messages: list[dict[str, Any]], config: str) -> dict[str, Any]: + """Count assistant bash commands that invoke the configured tool vs + fall back to plain text search. + + Returns {turns, tool_turns, fallback_turns, rate, fallback_rate}. + - rate = tool_turns / turns (None for baseline) + - fallback_rate = fallback_turns / turns (always reported; baseline's + fallback_rate is its raw grep/find usage and serves as a reference + point — tool tracks should be meaningfully below it). + + A low tool rate combined with a high fallback rate on a tool track + means the agent abandoned the tool and is operating as a baseline + with extra preamble. """ kws = TOOL_KEYWORDS.get(config, ()) - if not kws: - return {"turns": 0, "tool_turns": 0, "rate": None} turns = 0 tool_turns = 0 + fallback_turns = 0 for m in messages: if m.get("role") != "assistant": continue @@ -492,11 +503,22 @@ def compute_tool_usage(messages: list[dict[str, Any]], config: str) -> dict[str, args = fn.get("arguments") or "" if isinstance(args, dict): args = args.get("command", "") + if not isinstance(args, str): + args = str(args) turns += 1 - if any(kw in args for kw in kws): + if kws and any(kw in args for kw in kws): tool_turns += 1 - rate = tool_turns / turns if turns else None - return {"turns": turns, "tool_turns": tool_turns, "rate": rate} + if _FALLBACK_RE.search(args): + fallback_turns += 1 + rate = (tool_turns / turns) if (turns and kws) else None + fallback_rate = (fallback_turns / turns) if turns else None + return { + "turns": turns, + "tool_turns": tool_turns, + "rate": rate, + "fallback_turns": fallback_turns, + "fallback_rate": fallback_rate, + } def run_task( @@ -606,6 +628,8 @@ def run_task( metrics.tool_usage_rate = tool_usage["rate"] metrics.tool_usage_turns = tool_usage["tool_turns"] metrics.tool_usage_total = tool_usage["turns"] + metrics.fallback_turns = tool_usage["fallback_turns"] + metrics.fallback_rate = tool_usage["fallback_rate"] if exit_status == "error": metrics.outcome = "error" From e5f5631cc9afa7af64f17fd19edf18cbe08cae51 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 15:59:56 +0300 Subject: [PATCH 54/63] bench: add median wall-clock column to report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces median wall-clock seconds per task per config plus delta vs baseline, alongside tokens. wall_clock_sec was already captured in TaskMetrics — just plumbed into report aggregation/rendering. Sonnet 4.5 n=10: - baseline 336s — - code_graph 269s -20.1% - code_graph_mcp 273s -18.7% - lsp 290s -13.7% Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/report/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bench/report/__init__.py b/bench/report/__init__.py index f3c407b9..bc1d20e9 100644 --- a/bench/report/__init__.py +++ b/bench/report/__init__.py @@ -25,6 +25,7 @@ class ConfigSummary: p90_tokens: int median_tool_usage: float | None = None # fraction in [0,1] or None for baseline median_fallback: float | None = None # fraction in [0,1] of bash cmds that were grep/find/rg + median_wall_sec: float | None = None # median wall-clock seconds per task @property def resolve_rate(self) -> float: @@ -87,6 +88,10 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: r["fallback_rate"] for r in best_by_task.values() if r.get("fallback_rate") is not None ] + wall_secs = [ + r["wall_clock_sec"] for r in best_by_task.values() + if r.get("wall_clock_sec") is not None + ] n_resolved = sum(1 for r in best_by_task.values() if r.get("outcome") == "resolved") summaries.append( ConfigSummary( @@ -98,6 +103,7 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: p90_tokens=_percentile(token_sums, 0.9), median_tool_usage=statistics.median(usage_rates) if usage_rates else None, median_fallback=statistics.median(fallback_rates) if fallback_rates else None, + median_wall_sec=statistics.median(wall_secs) if wall_secs else None, ) ) return summaries @@ -115,19 +121,25 @@ def render_markdown(summaries: list[ConfigSummary]) -> str: baseline_med = baseline.median_tokens if baseline else 0 lines.append(f"## {bench}\n") - lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | tool-usage rate | fallback rate |") - lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|") + baseline_wall = baseline.median_wall_sec if baseline else None + lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | median wall (s) | Δ wall vs baseline | tool-usage rate | fallback rate |") + lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|") for s in sorted(group, key=lambda x: x.config): delta = "—" if baseline_med and s.config != "baseline": pct = (s.median_tokens - baseline_med) / baseline_med * 100 delta = f"{pct:+.1f}%" + wall = "—" if s.median_wall_sec is None else f"{s.median_wall_sec:.0f}" + wall_delta = "—" + if baseline_wall and s.median_wall_sec is not None and s.config != "baseline": + wp = (s.median_wall_sec - baseline_wall) / baseline_wall * 100 + wall_delta = f"{wp:+.1f}%" usage = "—" if s.median_tool_usage is None else f"{s.median_tool_usage * 100:.0f}%" fb = "—" if s.median_fallback is None else f"{s.median_fallback * 100:.0f}%" lines.append( f"| {s.config} | {s.n_tasks} | {s.n_resolved} | " f"{s.resolve_rate * 100:.1f}% | {s.median_tokens:,} | " - f"{s.p90_tokens:,} | {delta} | {usage} | {fb} |" + f"{s.p90_tokens:,} | {delta} | {wall} | {wall_delta} | {usage} | {fb} |" ) lines.append("") return "\n".join(lines) From 8dd305576c0d7a5338af46fafea1a9cf9d278469 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 16:06:07 +0300 Subject: [PATCH 55/63] bench: capture one-time indexing wall-clock per task _ensure_indexed and _ensure_indexed_mcp now return elapsed seconds (0.0 on cache hit). Runner stashes the value on the metrics row as index_sec; report renders median per config. This separates 'how long does indexing the repo take' (one-time setup cost) from 'how long does the agent take to solve the task' (the existing median wall column). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/metrics/__init__.py | 4 ++++ bench/report/__init__.py | 13 ++++++++++--- bench/runners/mini_runner.py | 28 ++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/bench/metrics/__init__.py b/bench/metrics/__init__.py index ac32421a..ec817c69 100644 --- a/bench/metrics/__init__.py +++ b/bench/metrics/__init__.py @@ -50,6 +50,10 @@ class TaskMetrics: # Always populated (incl. baseline as a reference point). fallback_rate: float | None = None fallback_turns: int = 0 + # One-time indexing wall-clock (only on first run per worktree). None + # for baseline/lsp (no indexing) and 0.0 when the graph was already + # built (cache hit on subsequent retries). + index_sec: float | None = None # outcome (set after scoring; None until then) outcome: str | None = None # "resolved" | "failed" | "budget_exceeded" | "error" | "tool_unavailable" diff --git a/bench/report/__init__.py b/bench/report/__init__.py index bc1d20e9..d967a6ef 100644 --- a/bench/report/__init__.py +++ b/bench/report/__init__.py @@ -26,6 +26,7 @@ class ConfigSummary: median_tool_usage: float | None = None # fraction in [0,1] or None for baseline median_fallback: float | None = None # fraction in [0,1] of bash cmds that were grep/find/rg median_wall_sec: float | None = None # median wall-clock seconds per task + median_index_sec: float | None = None # median one-time indexing seconds (cg / cg-mcp only) @property def resolve_rate(self) -> float: @@ -92,6 +93,10 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: r["wall_clock_sec"] for r in best_by_task.values() if r.get("wall_clock_sec") is not None ] + index_secs = [ + r["index_sec"] for r in best_by_task.values() + if r.get("index_sec") is not None + ] n_resolved = sum(1 for r in best_by_task.values() if r.get("outcome") == "resolved") summaries.append( ConfigSummary( @@ -104,6 +109,7 @@ def summarize(rows: list[dict[str, Any]]) -> list[ConfigSummary]: median_tool_usage=statistics.median(usage_rates) if usage_rates else None, median_fallback=statistics.median(fallback_rates) if fallback_rates else None, median_wall_sec=statistics.median(wall_secs) if wall_secs else None, + median_index_sec=statistics.median(index_secs) if index_secs else None, ) ) return summaries @@ -122,8 +128,8 @@ def render_markdown(summaries: list[ConfigSummary]) -> str: lines.append(f"## {bench}\n") baseline_wall = baseline.median_wall_sec if baseline else None - lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | median wall (s) | Δ wall vs baseline | tool-usage rate | fallback rate |") - lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|") + lines.append("| config | tasks | resolved | resolve rate | median tokens | p90 tokens | Δ tokens vs baseline | median wall (s) | Δ wall vs baseline | median index (s) | tool-usage rate | fallback rate |") + lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|") for s in sorted(group, key=lambda x: x.config): delta = "—" if baseline_med and s.config != "baseline": @@ -134,12 +140,13 @@ def render_markdown(summaries: list[ConfigSummary]) -> str: if baseline_wall and s.median_wall_sec is not None and s.config != "baseline": wp = (s.median_wall_sec - baseline_wall) / baseline_wall * 100 wall_delta = f"{wp:+.1f}%" + idx = "—" if s.median_index_sec is None else f"{s.median_index_sec:.1f}" usage = "—" if s.median_tool_usage is None else f"{s.median_tool_usage * 100:.0f}%" fb = "—" if s.median_fallback is None else f"{s.median_fallback * 100:.0f}%" lines.append( f"| {s.config} | {s.n_tasks} | {s.n_resolved} | " f"{s.resolve_rate * 100:.1f}% | {s.median_tokens:,} | " - f"{s.p90_tokens:,} | {delta} | {wall} | {wall_delta} | {usage} | {fb} |" + f"{s.p90_tokens:,} | {delta} | {wall} | {wall_delta} | {idx} | {usage} | {fb} |" ) lines.append("") return "\n".join(lines) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index ac2d3d44..7dfd19ad 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -260,7 +260,7 @@ def config_env(config: str, repo_path: Path) -> dict[str, str]: return env -def _ensure_indexed(repo_path: Path) -> None: +def _ensure_indexed(repo_path: Path) -> float: """Trigger /api/analyze_folder so `cg --repo ` returns data. The code-graph backend uses `Path(folder).name` as the repo identifier; @@ -268,8 +268,11 @@ def _ensure_indexed(repo_path: Path) -> None: `pytest-dev__pytest-6202__code_graph`, which becomes the `--repo` value the agent passes to `cg`. We skip indexing if the repo already exists in FalkorDB (cheap precheck against /api/list_repos). + + Returns wall-clock seconds spent indexing (0.0 if cache hit / skip). """ import httpx + start = time.monotonic() base = os.environ.get("CODEGRAPH_URL", "http://127.0.0.1:5000").rstrip("/") repo_name = repo_path.name @@ -280,7 +283,7 @@ def _ensure_indexed(repo_path: Path) -> None: r = c.get(f"{base}/api/list_repos") if r.status_code == 200 and repo_name in (r.json() or {}).get("repositories", []): print(f"[index] {repo_name} already indexed; skip") - return + return 0.0 print(f"[index] analyzing {repo_path} ...") # Default ignore set: auto-generated / vendored / pathological dirs # that either contain no useful symbols or send jedi into a @@ -301,21 +304,25 @@ def _ensure_indexed(repo_path: Path) -> None: f"analyze_folder returned {r.status_code}: {r.text[:300]}. " f"Check ALLOWED_ANALYSIS_DIR on the API server covers {repo_path}." ) - print(f"[index] indexed {repo_name}") + print(f"[index] indexed {repo_name} in {time.monotonic() - start:.1f}s") + return time.monotonic() - start except Exception as exc: raise RuntimeError(f"failed to index {repo_name} at {repo_path}: {exc}") from exc -def _ensure_indexed_mcp(repo_path: Path) -> None: +def _ensure_indexed_mcp(repo_path: Path) -> float: """MCP-track equivalent of _ensure_indexed. Drives the `index_repo` MCP tool in-process via the bench adapter (avoids spawning a second cgraph-mcp just to bootstrap; the agent will spawn its own per call). Same skip-if-present optimization as the HTTP path: cheap GRAPH.LIST scan against FalkorDB. + + Returns wall-clock seconds spent indexing (0.0 if cache hit / skip). """ from bench.agents import code_graph_mcp_adapter as cgm import redis + start = time.monotonic() repo_name = repo_path.name branch = os.environ.get("CGRAPH_MCP_BRANCH", "_default") @@ -326,7 +333,7 @@ def _ensure_indexed_mcp(repo_path: Path) -> None: r = redis.Redis(host=host, port=port, decode_responses=True, socket_timeout=2) if expected_graph in (r.execute_command("GRAPH.LIST") or []): print(f"[index-mcp] {expected_graph} already indexed; skip") - return + return 0.0 except Exception as exc: # noqa: BLE001 print(f"[index-mcp] WARN list_graphs failed ({exc!r}); will attempt index anyway") @@ -336,9 +343,10 @@ def _ensure_indexed_mcp(repo_path: Path) -> None: if isinstance(payload, dict) and payload.get("error"): print(f"[index-mcp] WARN index_repo error: {payload['error']!r}") else: - print(f"[index-mcp] indexed: {payload}") + print(f"[index-mcp] indexed in {time.monotonic() - start:.1f}s: {payload}") except Exception as exc: # noqa: BLE001 print(f"[index-mcp] WARN failed to index {repo_name}: {exc!r}") + return time.monotonic() - start # --------------------------------------------------------------------------- @@ -874,9 +882,11 @@ def main(argv: list[str] | None = None) -> int: # exist before the task runs, otherwise every `cg find-symbol` # call returns nothing and the agent abandons the tool. if cfg == "code_graph": - _ensure_indexed(cfg_wt) + index_sec = _ensure_indexed(cfg_wt) elif cfg == "code_graph_mcp": - _ensure_indexed_mcp(cfg_wt) + index_sec = _ensure_indexed_mcp(cfg_wt) + else: + index_sec = None cfg_rows = run_batch( [task], [cfg], @@ -891,6 +901,8 @@ def main(argv: list[str] | None = None) -> int: defer_jsonl=True, ) rows.extend(cfg_rows) + if cfg_rows and index_sec is not None: + cfg_rows[-1]["metrics"].index_sec = index_sec # Official SWE-bench harness verification. The agent's # patch is on the trajectory metrics; pass it to the # Docker-backed harness. When Docker is missing the From e82c05c4609e383307f9aa34b3274a206942526d Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 17:11:16 +0300 Subject: [PATCH 56/63] bench: robust indexing precheck (GRAPH.LIST + bounded timeout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP /api/list_repos response shape changed from [name, ...] to [{project, branch, graph}, ...], so the old 'repo_name in repositories' membership check silently returned False — every cg-track run re-issued analyze_folder even when the graph existed. With a 7200s timeout this masked server hangs for over an hour at a time. New precheck: - queries FalkorDB GRAPH.LIST directly (matches the MCP track path) - matches both bare-name (legacy) and code:: forms - bounded read timeout at 1800s (was 7200s); surfaces server hangs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 64 +++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 7dfd19ad..432b7234 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -266,35 +266,52 @@ def _ensure_indexed(repo_path: Path) -> float: The code-graph backend uses `Path(folder).name` as the repo identifier; each (instance, config) worktree has a unique directory name like `pytest-dev__pytest-6202__code_graph`, which becomes the `--repo` value - the agent passes to `cg`. We skip indexing if the repo already exists - in FalkorDB (cheap precheck against /api/list_repos). + the agent passes to `cg`. We skip indexing if the graph already exists + in FalkorDB (cheap GRAPH.LIST scan, matches the MCP-track behavior). Returns wall-clock seconds spent indexing (0.0 if cache hit / skip). """ import httpx + import redis start = time.monotonic() base = os.environ.get("CODEGRAPH_URL", "http://127.0.0.1:5000").rstrip("/") repo_name = repo_path.name token = os.environ.get("SECRET_TOKEN") or os.environ.get("CODEGRAPH_TOKEN") headers = {"Authorization": f"Bearer {token}"} if token else {} + + # Cheap precheck via FalkorDB GRAPH.LIST. The HTTP /api/list_repos + # path returned a list of names historically; it now returns dicts + # ({project, branch, graph}), so the old `name in repositories` + # match silently failed and every run re-indexed. GRAPH.LIST avoids + # that schema churn. + host = os.environ.get("FALKORDB_HOST", "127.0.0.1") + port = int(os.environ.get("FALKORDB_PORT", "6379")) + expected_graph = repo_name # the HTTP path uses bare folder name as graph key + try: + r = redis.Redis(host=host, port=port, decode_responses=True, socket_timeout=2) + graphs = r.execute_command("GRAPH.LIST") or [] + # Match either bare name (legacy) or "code::" pattern. + if expected_graph in graphs or any( + g == repo_name or g.startswith(f"code:{repo_name}:") for g in graphs + ): + print(f"[index] {repo_name} already in FalkorDB; skip") + return 0.0 + except Exception as exc: # noqa: BLE001 + print(f"[index] WARN GRAPH.LIST precheck failed ({exc!r}); attempting index anyway") + + print(f"[index] analyzing {repo_path} ...") + default_ignore = [ + ".git", "venv", ".venv", "node_modules", "__pycache__", + "rubi/rules", # sympy: blocks indexing for ~hours otherwise + "build", "dist", ".tox", ".eggs", + ] + # Bounded timeout so a server-side hang surfaces instead of stalling + # the entire benchmark. 30 min is generous for any sane repo and + # well below the previous 7200s that masked failures for an hour. try: - with httpx.Client(timeout=10.0, headers=headers) as c: - r = c.get(f"{base}/api/list_repos") - if r.status_code == 200 and repo_name in (r.json() or {}).get("repositories", []): - print(f"[index] {repo_name} already indexed; skip") - return 0.0 - print(f"[index] analyzing {repo_path} ...") - # Default ignore set: auto-generated / vendored / pathological dirs - # that either contain no useful symbols or send jedi into a - # multi-hour resolve loop (e.g. sympy/integrals/rubi/rules has - # 3000-line files with hundreds of unresolvable symbols per line). - default_ignore = [ - ".git", "venv", ".venv", "node_modules", "__pycache__", - "rubi/rules", # sympy: blocks indexing for ~hours otherwise - "build", "dist", ".tox", ".eggs", - ] - with httpx.Client(timeout=7200.0, headers=headers) as c: + with httpx.Client(timeout=httpx.Timeout(connect=10.0, read=1800.0, write=30.0, pool=10.0), + headers=headers) as c: r = c.post( f"{base}/api/analyze_folder", json={"path": str(repo_path), "ignore": default_ignore}, @@ -304,8 +321,15 @@ def _ensure_indexed(repo_path: Path) -> float: f"analyze_folder returned {r.status_code}: {r.text[:300]}. " f"Check ALLOWED_ANALYSIS_DIR on the API server covers {repo_path}." ) - print(f"[index] indexed {repo_name} in {time.monotonic() - start:.1f}s") - return time.monotonic() - start + elapsed = time.monotonic() - start + print(f"[index] indexed {repo_name} in {elapsed:.1f}s") + return elapsed + except httpx.ReadTimeout as exc: + elapsed = time.monotonic() - start + raise RuntimeError( + f"analyze_folder read-timeout after {elapsed:.0f}s on {repo_name} — " + f"API server likely hung indexing. Check uvicorn logs." + ) from exc except Exception as exc: raise RuntimeError(f"failed to index {repo_name} at {repo_path}: {exc}") from exc From 6508e3e9ae01e5b3275994be40ec0ae670b2ee27 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 18:14:51 +0300 Subject: [PATCH 57/63] fix(bench): add /api/_health probe + harness sanity check for tree-sitter resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the API server is launched without CODE_GRAPH_PY_RESOLVER=tree_sitter the PythonAnalyzer silently falls back to the jedi/multilspy path. On real-world repos (sphinx-doc/sphinx-8035, sympy, …) that path calls `python3 -m venv venv && pip install poetry && poetry install` per repo then runs jedi over the full transitive dep tree; we observed it wedge the server at 100% CPU + 3.5 GB RSS for 3+ hours with no progress. bench/scripts/start-api.sh already exports CODE_GRAPH_PY_RESOLVER, but a human-launched `uvicorn api.index:app …` won't pick it up and the bench silently degrades to the slow path. This commit makes the failure mode loud: 1. `GET /api/_health` returns {status, py_resolver, falkordb_host, falkordb_port, public}. Cheap (no DB call), unauth'd. 2. `_ensure_indexed` in the mini_runner calls /api/_health before any indexing and raises a clear RuntimeError when py_resolver != 'tree_sitter', pointing the operator at bench/scripts/start-api.sh. Verified: sphinx-doc__sphinx-8035 indexes in ~68s end-to-end with the new server (vs hours unbounded before). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/index.py | 19 +++++++++++++++++++ bench/runners/mini_runner.py | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/api/index.py b/api/index.py index b41023d6..95c6085f 100644 --- a/api/index.py +++ b/api/index.py @@ -113,6 +113,25 @@ class SwitchCommitRequest(BaseModel): # API routes # --------------------------------------------------------------------------- +@app.get('/api/_health') +async def _health(): + """Diagnostic endpoint: reports the running server's resolver + DB config. + + Used by the bench harness to fail-fast when the API server was started + without ``CODE_GRAPH_PY_RESOLVER=tree_sitter`` — without that flag the + Python indexer falls back to jedi/multilspy, which on real-world repos + (sphinx, sympy, …) spawns a per-repo venv + ``pip install poetry`` and + can wedge for hours at 100% CPU. Cheap (no DB call); safe to ship. + """ + return { + "status": "ok", + "py_resolver": os.environ.get("CODE_GRAPH_PY_RESOLVER", "jedi"), + "falkordb_host": os.environ.get("FALKORDB_HOST", "localhost"), + "falkordb_port": os.environ.get("FALKORDB_PORT", "6379"), + "public": os.environ.get("CODE_GRAPH_PUBLIC", "0"), + } + + @app.get('/api/graph_entities') async def graph_entities(repo: str = Query(None), branch: Optional[str] = Query(None), _=Depends(public_or_auth)): """Fetch sub-graph entities from a given repository.""" diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 432b7234..3fa3c589 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -280,6 +280,29 @@ def _ensure_indexed(repo_path: Path) -> float: token = os.environ.get("SECRET_TOKEN") or os.environ.get("CODEGRAPH_TOKEN") headers = {"Authorization": f"Bearer {token}"} if token else {} + # Sanity-check the server before we ask it to index anything. We've + # been bitten twice now by an API server launched without + # CODE_GRAPH_PY_RESOLVER=tree_sitter: the jedi/multilspy path tries + # to ``python -m venv venv && pip install poetry && poetry install`` + # per repo, then runs jedi over the full transitive dep tree. On + # sphinx-8035 that wedged the server at 100% CPU for 3h+. Refuse + # to proceed instead of letting it happen again. + try: + with httpx.Client(timeout=5.0, headers=headers) as c: + meta = c.get(f"{base}/api/_health").json() + except Exception as exc: # noqa: BLE001 + raise RuntimeError( + f"could not reach API server at {base}/api/_health ({exc!r}). " + "Start it with bench/scripts/start-api.sh." + ) from exc + if meta.get("py_resolver") != "tree_sitter": + raise RuntimeError( + f"API server at {base} is using py_resolver={meta.get('py_resolver')!r}. " + "The bench requires the tree-sitter static resolver — restart the " + "server with: CODE_GRAPH_PY_RESOLVER=tree_sitter " + "(bench/scripts/start-api.sh sets this by default)." + ) + # Cheap precheck via FalkorDB GRAPH.LIST. The HTTP /api/list_repos # path returned a list of names historically; it now returns dicts # ({project, branch, graph}), so the old `name in repositories` From 4c46736355377f26d152ef333fcaa7718a0d6f9d Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Thu, 28 May 2026 20:39:00 +0300 Subject: [PATCH 58/63] fix(bench): MCP adapter defaults CODE_GRAPH_PY_RESOLVER=tree_sitter + raises timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP adapter spawns a fresh cgraph-mcp stdio server per call. When the caller shell did not export CODE_GRAPH_PY_RESOLVER, the spawned server fell back to the legacy jedi/multilspy resolver, which runs 'python -m venv && pip install poetry && poetry install' per repo and then analyzes the full transitive dep tree. On full SWE-bench worktrees this wedges for >15 min — we observed it timing out indexing sympy__sympy-20154 and sympy__sympy-19040 during a fresh Opus calibration run. Mirror the start-api.sh policy: default CODE_GRAPH_PY_RESOLVER to tree_sitter in _env_for_mcp() so the MCP track is symmetric with HTTP regardless of caller env. Also bump the per-call timeout default 300s -> 900s in both the adapter (CGRAPH_MCP_TIMEOUT_SEC) and the cg-mcp CLI for headroom on cold MCP spawns over big repos. Validated: sympy-20154 (591 .py files, ~49k nodes, ~344k edges) indexes end-to-end via MCP in 220 s with the new default, vs >900 s timeout before. HTTP path on the same repo: 95 s; ~2.3x slower over the stdio spawn is expected and well within the new timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_mcp_adapter.py | 25 ++++++++++++++++++++++++- bench/cli/cg_mcp.py | 6 +++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/bench/agents/code_graph_mcp_adapter.py b/bench/agents/code_graph_mcp_adapter.py index 4cfcc5fe..900aa834 100644 --- a/bench/agents/code_graph_mcp_adapter.py +++ b/bench/agents/code_graph_mcp_adapter.py @@ -30,7 +30,21 @@ from mcp.client.stdio import stdio_client -DEFAULT_TIMEOUT_SEC = 300.0 +def _default_timeout() -> float: + """Pick the per-call MCP timeout, honoring CGRAPH_MCP_TIMEOUT_SEC. + + Defaults to 900 s (15 min). Bumped from the historical 300 s after + we observed `index_repo` on full SWE-bench worktrees (sphinx-doc, + sympy) finish in 5-10 min when spawned via the MCP stdio transport + rather than the warm in-process HTTP path. + """ + try: + return float(os.environ.get("CGRAPH_MCP_TIMEOUT_SEC", "900")) + except ValueError: + return 900.0 + + +DEFAULT_TIMEOUT_SEC = _default_timeout() def _env_for_mcp() -> dict[str, str]: @@ -39,10 +53,19 @@ def _env_for_mcp() -> dict[str, str]: Pass through everything from the caller but make sure the FalkorDB coordinates are present — the runner usually sets them to point at the host FalkorDB container. + + Also default ``CODE_GRAPH_PY_RESOLVER=tree_sitter``. Without this, + the spawned server falls back to the legacy jedi/multilspy path, + which does ``python -m venv && pip install poetry && poetry install`` + per repo before analyzing the transitive dep tree — wedges for hours + on large SWE-bench repos (sphinx, sympy). The HTTP track gets this + via ``bench/scripts/start-api.sh``; we mirror it here so the MCP + track is symmetric regardless of how the caller shell is configured. """ env = dict(os.environ) env.setdefault("FALKORDB_HOST", os.environ.get("FALKORDB_HOST", "127.0.0.1")) env.setdefault("FALKORDB_PORT", os.environ.get("FALKORDB_PORT", "6379")) + env.setdefault("CODE_GRAPH_PY_RESOLVER", "tree_sitter") return env diff --git a/bench/cli/cg_mcp.py b/bench/cli/cg_mcp.py index 31dc6234..896f7b44 100644 --- a/bench/cli/cg_mcp.py +++ b/bench/cli/cg_mcp.py @@ -26,7 +26,7 @@ Env: FALKORDB_HOST / FALKORDB_PORT are passed through to the spawned server. Optionally set CGRAPH_MCP_TIMEOUT_SEC to override the -default 60s timeout. +default 900s timeout. """ from __future__ import annotations @@ -47,9 +47,9 @@ def _print(obj: Any) -> None: def _timeout() -> float: try: - return float(os.getenv("CGRAPH_MCP_TIMEOUT_SEC", "300")) + return float(os.getenv("CGRAPH_MCP_TIMEOUT_SEC", "900")) except ValueError: - return 60.0 + return 900.0 def _add_project(p: argparse.ArgumentParser) -> None: From dc8534ea19c2ef67ef10b55a11f4d7e96428b7ea Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Fri, 29 May 2026 00:44:53 +0300 Subject: [PATCH 59/63] perf(bench): compact cg/cg-mcp output + trim system preambles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus n=10 calibration showed the code_graph track spending +14.5% input tokens vs baseline, and code_graph_mcp +19.5% — driven by three things that compound over a 70-80-turn trajectory: 1. /api/get_neighbors returns a verbose vars(node)/vars(edge) dump that includes empty 'properties: {}' and empty 'alias: ""' on every edge, plus per-node 'doc' blocks. Every byte we hand back is re-fed to the LLM on every subsequent turn, so a single 20 KB neighbors call ends up billed ~50x. 2. JSON was pretty-printed (indent=2). Whitespace is free for humans, not for token counts. 3. System preambles were 2.8-3.6 KB of duplicated mini-swe-agent submission boilerplate + repeated rules-of-thumb. This is a bench-layer-only change (the React frontend and the core API contract are untouched): - bench/cli/cg.py: - _compact_neighbors strips empty properties/alias and projects nodes to {id, label, name, file, line}. - _compact_symbols same for find-symbol/auto-complete. - New --limit flag on get-neighbors (default 50; 0 = unlimited). - JSON now emits with separators=(',', ':'). - bench/cli/cg_mcp.py: same compact JSON formatting. - system_preamble.md (code_graph, code_graph_mcp): rewritten as ~1.1 KB instead of 2.9-3.6 KB, keeping the workflow and sub-command listing but dropping mini-swe-agent's own submission boilerplate. Validated against the live indexed graphs: - pytest-6202 cold neighbor call: 400 -> 148 bytes (63%) - sympy-19040 hot neighbor (id=23432, 2039 outgoing edges): raw: 747 KB -> compacted unlim: 252 KB (-66%) -> compacted lim=50: 6.2 KB (-99.2%) - sympy-19040 auto-complete prefix='solve': 11.4 KB -> 0.6 KB (-94.6%) Expected effect on the next Opus run: cg* input-token cost drops below baseline rather than above it, while the agent still gets the same structural information. All 24 per-branch-graph tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/cli/cg.py | 84 ++++++++++++++- bench/cli/cg_mcp.py | 4 +- bench/tools/code_graph/system_preamble.md | 95 +++++------------ bench/tools/code_graph_mcp/system_preamble.md | 100 ++++-------------- 4 files changed, 131 insertions(+), 152 deletions(-) diff --git a/bench/cli/cg.py b/bench/cli/cg.py index 4b74be0e..e31e623e 100644 --- a/bench/cli/cg.py +++ b/bench/cli/cg.py @@ -24,12 +24,85 @@ import argparse import json import sys +from typing import Any from bench.agents.code_graph_adapter import CodeGraphClient +# ---------- Output compaction -------------------------------------------------- +# Every byte returned here is re-fed to the LLM on every subsequent turn (the +# context window grows monotonically until the trajectory ends). A neighbors +# call that returns 20 KB of raw JSON costs ~5K tokens, and at 50+ turns that +# compounds badly. The full FastAPI shape is needed by the React frontend, not +# by an agent — strip the noise here so the LLM sees only what it can act on. + +_NODE_KEEP = ("id", "label", "labels", "name", "file", "src", "line", "start_line", "end_line") +_EDGE_KEEP = ("id", "src_node", "dest_node", "relation") + + +def _compact_node(n: Any) -> Any: + if not isinstance(n, dict): + return n + out: dict[str, Any] = {} + props = n.get("properties") or {} + for k in _NODE_KEEP: + if k in n and n[k] not in (None, "", [], {}): + out[k] = n[k] + elif k in props and props[k] not in (None, "", [], {}): + out[k] = props[k] + return out + + +def _compact_edge(e: Any) -> Any: + if not isinstance(e, dict): + return e + out: dict[str, Any] = {} + for k in _EDGE_KEEP: + v = e.get(k) + if v not in (None, "", [], {}): + out[k] = v + return out + + +def _compact_neighbors(payload: dict[str, Any], limit: int | None) -> dict[str, Any]: + """Strip empty properties + alias and apply optional limit.""" + if not isinstance(payload, dict): + return payload + n = payload.get("neighbors") or payload + nodes = [_compact_node(x) for x in (n.get("nodes") or [])] + edges = [_compact_edge(x) for x in (n.get("edges") or [])] + if limit is not None and limit > 0: + nodes = nodes[:limit] + edges = edges[:limit] + out: dict[str, Any] = {"nodes": nodes, "edges": edges} + if "branch" in payload: + out["branch"] = payload["branch"] + return out + + +def _compact_symbols(payload: Any) -> Any: + """Trim find-symbol / auto-complete records to the fields the agent needs. + + The HTTP responses vary in shape: + - find_symbol: ``[node, ...]`` + - auto_complete: ``{"branch": ..., "completions": [node, ...]}`` + Compact both consistently. + """ + if isinstance(payload, list): + return [_compact_node(x) for x in payload] + if isinstance(payload, dict): + for key in ("completions", "results", "matches", "items"): + if key in payload: + out = {k: v for k, v in payload.items() if k != key} + out[key] = [_compact_node(x) for x in (payload[key] or [])] + return out + return payload + + def _print(obj: object) -> None: - json.dump(obj, sys.stdout, indent=2, sort_keys=True) + # Compact separators shave ~30 % off vs the default indented form, which the + # LLM doesn't need (it ignores whitespace). + json.dump(obj, sys.stdout, separators=(",", ":"), sort_keys=True, default=str) sys.stdout.write("\n") @@ -46,6 +119,8 @@ def add_repo(p: argparse.ArgumentParser) -> None: gn = sub.add_parser("get-neighbors") add_repo(gn) gn.add_argument("--ids", type=int, nargs="+", required=True) + gn.add_argument("--limit", type=int, default=50, + help="cap nodes/edges in response (default 50, 0 = unlimited)") fp = sub.add_parser("find-paths") add_repo(fp) @@ -70,13 +145,14 @@ def add_repo(p: argparse.ArgumentParser) -> None: if args.cmd == "graph-entities": _print(c.graph_entities(args.repo)) elif args.cmd == "get-neighbors": - _print(c.get_neighbors(args.repo, args.ids)) + limit = args.limit if args.limit > 0 else None + _print(_compact_neighbors(c.get_neighbors(args.repo, args.ids), limit)) elif args.cmd == "find-paths": _print(c.find_paths(args.repo, args.src, args.dst)) elif args.cmd == "auto-complete": - _print(c.auto_complete(args.repo, args.prefix)) + _print(_compact_symbols(c.auto_complete(args.repo, args.prefix))) elif args.cmd == "find-symbol": - _print(c.find_symbol(args.repo, args.name)) + _print(_compact_symbols(c.find_symbol(args.repo, args.name))) elif args.cmd == "note-edit": _print(c.note_edit(args.repo, args.path)) else: diff --git a/bench/cli/cg_mcp.py b/bench/cli/cg_mcp.py index 896f7b44..7079c598 100644 --- a/bench/cli/cg_mcp.py +++ b/bench/cli/cg_mcp.py @@ -41,7 +41,9 @@ def _print(obj: Any) -> None: - json.dump(obj, sys.stdout, indent=2, sort_keys=True, default=str) + # Compact JSON: agents don't care about indentation, and every byte we + # save here is re-fed to the LLM every subsequent turn. + json.dump(obj, sys.stdout, separators=(",", ":"), sort_keys=True, default=str) sys.stdout.write("\n") diff --git a/bench/tools/code_graph/system_preamble.md b/bench/tools/code_graph/system_preamble.md index 75bfa0ac..5ed0c6ad 100644 --- a/bench/tools/code_graph/system_preamble.md +++ b/bench/tools/code_graph/system_preamble.md @@ -1,72 +1,27 @@ # code-graph preamble -You are an autonomous coding agent solving a software-engineering task. -Your sole tool is bash: every action you take is a shell command that -is executed in the repository's working directory. - -## Code-navigation workflow — use this BEFORE grep/find - -A code-graph service is indexed for this repository. **Before reading or -editing code, locate the relevant symbols through `cg` rather than -grepping the file tree** — it's faster, returns precise file:line -records, and reveals call/definition relationships you would otherwise -have to reconstruct by hand. Fall back to bash only when `cg` cannot -answer the question. - -Typical loop: - -1. `cg find-symbol --repo "$REPO_NAME" --name ` to locate a - function/class by name. -2. `cg get-neighbors --repo "$REPO_NAME" --ids ` to see callers, - callees, and definitions. -3. Read the implicated file(s) with `sed -n` / `cat`, then edit. -4. After every file edit, run - `cg note-edit --repo "$REPO_NAME" --path ` so subsequent - graph queries reflect your change. - -`$REPO_NAME` is exported for you (do not guess). - -## Available `cg` sub-commands - -- `cg find-symbol --repo REPO --name NAME` — locate node(s) for a - symbol by name. Returns `{id, label, file, line}` records. -- `cg get-neighbors --repo REPO --ids N [N ...]` — direct neighbors - in the knowledge graph (callers, callees, definitions). -- `cg find-paths --repo REPO --src N --dst N` — paths between two - nodes. -- `cg graph-entities --repo REPO` — paginated sub-graph dump (large). -- `cg auto-complete --repo REPO --prefix STRING` — prefix search. -- `cg note-edit --repo REPO --path PATH` — re-index a file after - you edit it. **Call this after every edit.** - -You also have the usual Unix tools (`cat`, `grep`/`rg`, `find`, `sed`) -for cases the graph can't answer. - -## Rules of thumb - -1. **At least one `cg` call before any source edit.** Locate the - symbol via `find-symbol` or `auto-complete` before reading files. -2. **Do not fall back to `grep`/`rg`/`find` silently.** Your - trajectory is being measured for tool-usage rate. If `cg` errors - or returns nothing, state that explicitly in your next message - (e.g. "cg find-symbol returned empty for X, falling back to rg") - BEFORE running the fallback. Trajectories where you abandon `cg` - after one failure without explanation are flagged as invalid - measurements. -3. **Don't `grep` for callers.** `get-neighbors` is one cheap - Cypher hop; grep over a large repo costs tens of thousands of - tokens. - -## Submission - -When you believe the task is complete, run a bash command whose first -line of stdout is exactly: - -``` -COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT -``` - -followed by your final answer or summary on subsequent lines. The -runner reads the working-tree `git diff` automatically; you do not -need to commit. - +A pre-indexed code-graph for this repo is available via `cg`. +**Use `cg` to locate symbols before reading files or grepping.** +`$REPO_NAME` is exported. + +## Workflow + +1. `cg find-symbol --repo "$REPO_NAME" --name ` → `{id, file, line}`. +2. `cg get-neighbors --repo "$REPO_NAME" --ids [--limit 50]` → + callers / callees / definitions. Default limit 50 keeps output small; + pass `--limit 0` only if you truly need everything. +3. Read the file with `sed -n` / `cat`, then edit. +4. After every edit run `cg note-edit --repo "$REPO_NAME" --path `. + +## Sub-commands + +- `cg find-symbol --repo R --name NAME` +- `cg get-neighbors --repo R --ids N [N ...] [--limit N]` +- `cg find-paths --repo R --src N --dst N` +- `cg auto-complete --repo R --prefix STRING` +- `cg note-edit --repo R --path PATH` (call after every edit) +- `cg graph-entities --repo R` (large; rarely needed) + +Standard Unix tools (`cat`, `grep`, `find`, `sed`) remain available for +cases the graph can't answer. If `cg` returns empty, say so before +falling back. diff --git a/bench/tools/code_graph_mcp/system_preamble.md b/bench/tools/code_graph_mcp/system_preamble.md index 4b97b9b4..4ed13848 100644 --- a/bench/tools/code_graph_mcp/system_preamble.md +++ b/bench/tools/code_graph_mcp/system_preamble.md @@ -1,82 +1,28 @@ # code-graph (MCP) preamble -You are an autonomous coding agent solving a software-engineering task. -Your sole tool is bash: every action you take is a shell command that -is executed in the repository's working directory. +A pre-indexed code-graph for this repo is available via the +`cg-mcp` CLI (talks to `cgraph-mcp` over stdio). +**Use `cg-mcp` to locate symbols before reading files or grepping.** +`$PROJECT_NAME` and `$BRANCH` are exported. -## Code-navigation workflow — use this BEFORE grep/find +## Workflow -A code-graph **MCP server** (`cgraph-mcp`) is available for this repo. -**Before reading or editing code, locate the relevant symbols through -`cg-mcp` rather than grepping the file tree** — it's faster, returns -precise `{id, file, line}` records, and reveals caller / callee / -impact relationships you would otherwise reconstruct by hand. Fall -back to bash only when `cg-mcp` cannot answer the question. - -`$PROJECT_NAME` and `$BRANCH` are exported for you (do not guess). -The graph is already indexed against the current commit. - -Typical loop: - -1. `cg-mcp search_code --project "$PROJECT_NAME" --prefix ` — - locate a function/class by name. Pick the `id` of the best hit. +1. `cg-mcp search_code --project "$PROJECT_NAME" --prefix ` → + list of `{id, name, file, line}`. Pick the best `id`. 2. `cg-mcp get_callers --project "$PROJECT_NAME" --symbol-id ` — - "who calls this?" before refactoring. -3. `cg-mcp impact_analysis --project "$PROJECT_NAME" --symbol-id - --depth 3` — full transitive blast radius. Use this BEFORE - non-trivial edits. -4. Read the implicated file(s) with `sed -n` / `cat`, then edit. - -## Available `cg-mcp` sub-commands - -- `cg-mcp search_code --project P --prefix STR [--limit N]` — - prefix search; returns `[{id, name, label, file, line}, ...]`. -- `cg-mcp get_callers --project P --symbol-id ID [--limit N]` — - incoming CALLS edges (who calls X). -- `cg-mcp get_callees --project P --symbol-id ID [--limit N]` — - outgoing CALLS edges (what X calls). -- `cg-mcp get_dependencies --project P --symbol-id ID [--limit N]` — - all outgoing edges (CALLS + IMPORTS + DEFINES). -- `cg-mcp impact_analysis --project P --symbol-id ID - [--direction IN|OUT] [--depth N]` — - transitive blast radius (default IN, depth 3). -- `cg-mcp find_path --project P --source-id ID --dest-id ID` — - the call chain(s) between two symbols. -- `cg-mcp index_repo --path-or-url PATH [--branch B]` — - (re)index a folder or git URL. Only needed for repos that aren't - pre-indexed. - -You also have the usual Unix tools (`cat`, `grep`/`rg`, `find`, `sed`) -for cases the graph can't answer. - -## Rules of thumb - -1. **Always run `search_code` first** to turn a name into an `id`. -2. **`impact_analysis` before any non-trivial edit.** Even when you - think you know the answer — the transitive closure often surprises - you. -3. **Don't `grep` for callers.** `get_callers` is one cheap Cypher - hop; grep over a large repo costs tens of thousands of tokens. -4. **Do not fall back to `grep`/`rg`/`find` silently.** Your trajectory - is being measured for tool-usage rate. If `cg-mcp` returns no - results or errors, state that explicitly in your next message - (e.g. "cg-mcp returned empty for prefix X, falling back to rg") - BEFORE running the fallback. Trajectories where you abandon - `cg-mcp` after one failure without explanation are flagged as - invalid measurements. -5. **At least one `cg-mcp` call before any source edit.** If you - cannot locate the symbol via `search_code`, document why before - resorting to text search. - -## Submission - -When you believe the task is complete, run a bash command whose first -line of stdout is exactly: - -``` -COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT -``` - -followed by your final answer or summary on subsequent lines. The -runner reads the working-tree `git diff` automatically; you do not -need to commit. + who calls X. (Default `--limit 50`.) +3. `cg-mcp impact_analysis --project "$PROJECT_NAME" --symbol-id --depth 3` — + transitive blast radius before any non-trivial edit. +4. Read the file with `sed -n` / `cat`, then edit. + +## Sub-commands + +- `cg-mcp search_code --project P --prefix STR [--limit N]` +- `cg-mcp get_callers --project P --symbol-id ID [--limit N]` +- `cg-mcp get_callees --project P --symbol-id ID [--limit N]` +- `cg-mcp get_dependencies --project P --symbol-id ID [--limit N]` +- `cg-mcp impact_analysis --project P --symbol-id ID [--direction IN|OUT] [--depth N]` +- `cg-mcp find_path --project P --source-id ID --dest-id ID` + +Standard Unix tools remain available. If `cg-mcp` returns empty, say so +before falling back to grep. From 805d0add1f02239ce0b36bb10b228a2d45469c0e Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Fri, 29 May 2026 01:05:13 +0300 Subject: [PATCH 60/63] fix(bench): restore submission sentinel in cg/cg-mcp preambles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compaction pass in dc8534e accidentally dropped the COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT submission instruction (which baseline + lsp still had). Without it Opus has no way to signal 'done' and just loops, re-emitting the final diff every turn. Visible in opus-smoke n=1 (pytest-6202) after dc8534e: config msgs bash_outs input_tok vs baseline baseline 60 28 243k — lsp 52 24 209k -14% code_graph 235 82 1,043k +329% <-- looped code_graph_mcp hung > 16min on Azure round-trip Restore the autonomous-agent framing intro + submission section in both preambles. Keep the trimmed workflow / sub-command list. Also add an explicit anti-loop rule ('do not call the same cg query twice for the same symbol'). Net: ~30% smaller than original (was 60% with the broken trim), and now has the must-have completion contract. Compaction of tool *output* (in cg.py / cg_mcp.py) is unchanged and still valid; this only touches the preambles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/tools/code_graph/system_preamble.md | 30 +++++++++++++++++-- bench/tools/code_graph_mcp/system_preamble.md | 29 ++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/bench/tools/code_graph/system_preamble.md b/bench/tools/code_graph/system_preamble.md index 5ed0c6ad..99ad597b 100644 --- a/bench/tools/code_graph/system_preamble.md +++ b/bench/tools/code_graph/system_preamble.md @@ -1,5 +1,9 @@ # code-graph preamble +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + A pre-indexed code-graph for this repo is available via `cg`. **Use `cg` to locate symbols before reading files or grepping.** `$REPO_NAME` is exported. @@ -22,6 +26,26 @@ A pre-indexed code-graph for this repo is available via `cg`. - `cg note-edit --repo R --path PATH` (call after every edit) - `cg graph-entities --repo R` (large; rarely needed) -Standard Unix tools (`cat`, `grep`, `find`, `sed`) remain available for -cases the graph can't answer. If `cg` returns empty, say so before -falling back. +## Rules + +- **Do not call the same `cg` query twice for the same symbol.** + Cache the result mentally; if you need it again, re-read the + earlier tool output in this conversation. +- **Do not fall back to `grep`/`rg`/`find` silently.** If `cg` + returns empty, say so in your next message before grepping. +- Standard Unix tools (`cat`, `grep`, `find`, `sed`) remain available + for cases the graph can't answer. + +## Submission + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. **Once you emit this sentinel, stop — do not re-emit +the diff or run further commands.** diff --git a/bench/tools/code_graph_mcp/system_preamble.md b/bench/tools/code_graph_mcp/system_preamble.md index 4ed13848..bf2faf32 100644 --- a/bench/tools/code_graph_mcp/system_preamble.md +++ b/bench/tools/code_graph_mcp/system_preamble.md @@ -1,5 +1,9 @@ # code-graph (MCP) preamble +You are an autonomous coding agent solving a software-engineering task. +Your sole tool is bash: every action you take is a shell command that +is executed in the repository's working directory. + A pre-indexed code-graph for this repo is available via the `cg-mcp` CLI (talks to `cgraph-mcp` over stdio). **Use `cg-mcp` to locate symbols before reading files or grepping.** @@ -24,5 +28,26 @@ A pre-indexed code-graph for this repo is available via the - `cg-mcp impact_analysis --project P --symbol-id ID [--direction IN|OUT] [--depth N]` - `cg-mcp find_path --project P --source-id ID --dest-id ID` -Standard Unix tools remain available. If `cg-mcp` returns empty, say so -before falling back to grep. +## Rules + +- **Do not call the same `cg-mcp` query twice for the same symbol.** + Cache the result mentally; if you need it again, re-read the + earlier tool output in this conversation. +- **Do not fall back to `grep`/`rg`/`find` silently.** If `cg-mcp` + returns empty, say so in your next message before grepping. +- Standard Unix tools remain available for cases the graph can't + answer. + +## Submission + +When you believe the task is complete, run a bash command whose first +line of stdout is exactly: + +``` +COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT +``` + +followed by your final answer or summary on subsequent lines. The +runner reads the working-tree `git diff` automatically; you do not +need to commit. **Once you emit this sentinel, stop — do not re-emit +the diff or run further commands.** From 4758ea1d0c399fa13c530ee6a7ced5547b774090 Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Fri, 29 May 2026 06:26:02 +0300 Subject: [PATCH 61/63] fix(bench): SIGKILL whole pgid on timeout to stop orphan leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LocalEnvironment.execute uses subprocess.run(shell=True, timeout=N). On timeout Python kills only the immediate shell PID — grandchildren (typically the python -c snippets the agent uses to reproduce bugs) get reparented to init and run forever. The Opus n=10 run leaked 4 such pythons from sympy-19040, each at ~100% CPU for 3-4 hours after the trajectory ended (~13 CPU-hours). Confirmed locally with a reproducer that left PPID=1 python alive under upstream LocalEnvironment. SafeLocalEnvironment spawns each command via Popen(start_new_session=True) so it gets its own process group, then on timeout os.killpg(pgid, SIGKILL) takes the whole subtree down. Output / returncode shape is otherwise identical to upstream so trajectories remain comparable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/runners/mini_runner.py | 6 +- bench/runners/safe_local_env.py | 128 ++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 bench/runners/safe_local_env.py diff --git a/bench/runners/mini_runner.py b/bench/runners/mini_runner.py index 3fa3c589..2e3d441f 100644 --- a/bench/runners/mini_runner.py +++ b/bench/runners/mini_runner.py @@ -602,7 +602,9 @@ def run_task( # Late imports — they trigger litellm side effects. from minisweagent.agents.default import DefaultAgent - from minisweagent.environments.local import LocalEnvironment + from minisweagent.environments.local import LocalEnvironment # noqa: F401 (still referenced in docstring) + + from bench.runners.safe_local_env import SafeLocalEnvironment env_vars = config_env(config, task.repo_path) @@ -627,7 +629,7 @@ def run_task( "diff": "", } - env = LocalEnvironment(cwd=str(task.repo_path), env=env_vars, timeout=120) + env = SafeLocalEnvironment(cwd=str(task.repo_path), env=env_vars, timeout=120) preamble = load_preamble(config) if dry_run: diff --git a/bench/runners/safe_local_env.py b/bench/runners/safe_local_env.py new file mode 100644 index 00000000..a218df0a --- /dev/null +++ b/bench/runners/safe_local_env.py @@ -0,0 +1,128 @@ +"""Bench-only LocalEnvironment that kills the whole process group on timeout. + +Why this file exists +-------------------- + +``minisweagent.environments.local.LocalEnvironment.execute`` calls +``subprocess.run(shell=True, timeout=N)``. On timeout, Python sends +SIGKILL to the immediate shell PID — but any child the agent spawned +inside that shell (e.g. ``bash -c 'python -c "from sympy import ..."'``) +becomes orphaned and reparented to PID 1. We caught four such orphans +in the n=10 Opus run, each pegged at ~100% CPU for **3-4 hours** +after the parent trajectory had long since exited — all +``python -c "from sympy import *; factor(..., extension=[I])"`` +snippets the agent had run to reproduce the very infinite-loop bug it +was trying to fix in sympy-19040. + +Fix: spawn each command via ``Popen(start_new_session=True)`` so it +gets its own process group, then on timeout ``os.killpg(pgid, +SIGKILL)`` so every descendant dies. Output / returncode handling is +otherwise identical to upstream so trajectories remain comparable. +""" + +from __future__ import annotations + +import os +import platform +import signal +import subprocess +from typing import Any + +from minisweagent.environments.local import LocalEnvironment, LocalEnvironmentConfig +from minisweagent.exceptions import Submitted +from minisweagent.utils.serialize import recursive_merge + + +class SafeLocalEnvironment(LocalEnvironment): + """LocalEnvironment that SIGKILLs the whole process group on timeout.""" + + def execute(self, action: dict, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]: + command = action.get("command", "") + cwd = cwd or self.config.cwd or os.getcwd() + effective_timeout = timeout or self.config.timeout + + proc = subprocess.Popen( + command, + shell=True, + text=True, + cwd=cwd, + env=os.environ | self.config.env, + encoding="utf-8", + errors="replace", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + # Put the shell — and every descendant — in its own process + # group so we can SIGKILL the whole tree on timeout. + start_new_session=True, + ) + + try: + stdout, _ = proc.communicate(timeout=effective_timeout) + output = { + "output": stdout or "", + "returncode": proc.returncode, + "exception_info": "", + } + except subprocess.TimeoutExpired as e: + # The shell ignored its deadline (or the agent's command + # double-forked). Kill the *group* so orphaned children + # (e.g. `python -c "from sympy import ..."`) die too. + self._kill_process_group(proc.pid) + try: + stdout, _ = proc.communicate(timeout=5) + except Exception: + stdout = "" + raw = e.output or stdout or "" + if isinstance(raw, bytes): + raw = raw.decode("utf-8", errors="replace") + output = { + "output": raw, + "returncode": -1, + "exception_info": f"An error occurred while executing the command: {e}", + "extra": { + "exception_type": type(e).__name__, + "exception": str(e), + "killed_process_group": True, + }, + } + except Exception as e: + # Best-effort: still try to kill anything we spawned. + try: + self._kill_process_group(proc.pid) + except Exception: + pass + raw = getattr(e, "output", None) or "" + if isinstance(raw, bytes): + raw = raw.decode("utf-8", errors="replace") + output = { + "output": raw, + "returncode": -1, + "exception_info": f"An error occurred while executing the command: {e}", + "extra": {"exception_type": type(e).__name__, "exception": str(e)}, + } + + self._check_finished(output) + return output + + @staticmethod + def _kill_process_group(pid: int) -> None: + try: + pgid = os.getpgid(pid) + except ProcessLookupError: + return + for sig in (signal.SIGTERM, signal.SIGKILL): + try: + os.killpg(pgid, sig) + except ProcessLookupError: + return + except PermissionError: + return + # Give SIGTERM a brief moment before escalating to SIGKILL. + if sig is signal.SIGTERM: + try: + os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + pass + + +__all__ = ["SafeLocalEnvironment", "LocalEnvironmentConfig"] From 31259463d7ee016069a0bcfd7e685f362b3d903b Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Fri, 29 May 2026 06:41:17 +0300 Subject: [PATCH 62/63] fix(bench/mcp): collect ALL TextContent chunks, prefer structuredContent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastMCP serializes list[dict] tool returns as N separate TextContent chunks (one per item) AND echoes the full list in structuredContent['result']. Our previous _extract returned only the FIRST text chunk, which meant every list-returning tool (search_code, get_callers, get_callees, get_dependencies, impact_analysis, find_path) silently truncated to its first element throughout the entire benchmark. Caught on the n=10 Opus run, sympy-19040 trajectory: agent ran search_code(prefix='factor') hoping to discover dmp_ext_factor, dmp_sqf_part, factor_list, etc. — got back only 'factor_nc' (alphabetically first), retried with --limit 30 (same single result), gave up on the graph entirely and burned 50 python/pytest turns flailing in bash. cg_mcp ran 95 turns vs cg's 73 on the same task, 2.89M vs 1.39M input tokens. Fix: prefer structuredContent (always carries the full payload), unwrapping the spec's {'result': ...} envelope. Fall back to concatenating + parsing all text chunks so older FastMCP versions still work. Two new regression tests pin the multi-chunk shape; all 8 existing tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/agents/code_graph_mcp_adapter.py | 50 ++++++++++++++++++-------- tests/bench/test_cg_mcp_adapter.py | 28 +++++++++++++++ 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/bench/agents/code_graph_mcp_adapter.py b/bench/agents/code_graph_mcp_adapter.py index 900aa834..31b7c8b7 100644 --- a/bench/agents/code_graph_mcp_adapter.py +++ b/bench/agents/code_graph_mcp_adapter.py @@ -72,23 +72,43 @@ def _env_for_mcp() -> dict[str, str]: def _extract(result: Any) -> Any: """Normalize a CallToolResult into a JSON-serialisable Python value. - The MCP spec lets servers put the payload in `structuredContent` - and/or echo it as a JSON text chunk. Our 8 tools do both; agents - have historically preferred the text payload. We mirror that: - return the parsed text chunk when present, otherwise fall back to - structuredContent (unwrapping the spec's `{"result": ...}` wrapper - for collection-returning tools). + FastMCP serializes list returns as **N separate TextContent + chunks** (one per item) AND echoes the full list in + ``structuredContent['result']``. Earlier versions of this helper + returned only the *first* TextContent chunk, which meant every + list-returning tool (``search_code``, ``get_callers``, + ``get_callees``, ``get_dependencies``, ``impact_analysis``, + ``find_path``) silently returned just the first element. On + sympy-19040 the Opus agent searched "factor", got back one record + instead of ten, gave up on the graph entirely and burned ~50 turns + on bash exploration — cg_mcp ended up at +35% vs baseline. + + The new policy: prefer ``structuredContent`` when present (it + always carries the full payload), unwrapping the spec's + ``{"result": ...}`` envelope. Fall back to concatenating the text + chunks (which may be JSON-each or a single JSON document). """ - for chunk in result.content: - if hasattr(chunk, "text") and chunk.text: - try: - return json.loads(chunk.text) - except json.JSONDecodeError: - return chunk.text struct = getattr(result, "structuredContent", None) - if isinstance(struct, dict) and set(struct.keys()) == {"result"}: - return struct["result"] - return struct + if isinstance(struct, dict): + if set(struct.keys()) == {"result"}: + return struct["result"] + return struct + + chunks: list[str] = [c.text for c in (result.content or []) if getattr(c, "text", None)] + if not chunks: + return None + # Try parsing each chunk as JSON and assembling them. If every + # chunk parses, we likely had a per-item list serialization; if + # only one chunk parses, that's the whole payload. + parsed: list[Any] = [] + for c in chunks: + try: + parsed.append(json.loads(c)) + except json.JSONDecodeError: + return "\n".join(chunks) + if len(parsed) == 1: + return parsed[0] + return parsed async def _call_tool_async(name: str, arguments: dict[str, Any], timeout: float) -> Any: diff --git a/tests/bench/test_cg_mcp_adapter.py b/tests/bench/test_cg_mcp_adapter.py index 7d6f9274..6a1ad98d 100644 --- a/tests/bench/test_cg_mcp_adapter.py +++ b/tests/bench/test_cg_mcp_adapter.py @@ -79,6 +79,34 @@ def test_extract_returns_raw_text_when_not_json(): assert cgm._extract(r) == "not json at all" +def test_extract_returns_full_list_from_structured_content(): + """REGRESSION: FastMCP serializes ``list[dict]`` returns as N TextContent + chunks (one per item) AND the full list in ``structuredContent['result']``. + Earlier the extractor returned only the first text chunk, silently + truncating every list-returning tool (``search_code``, ``get_callers``, + ``impact_analysis``, …) to its first element. We caught this on the n=10 + Opus run: cg_mcp burned +35% input tokens vs baseline because the agent + kept seeing 1-element results, gave up on the graph, and flailed in bash. + """ + chunks = [_FakeChunk(json.dumps({"id": i, "name": f"x_{i}"})) for i in range(10)] + struct = {"result": [{"id": i, "name": f"x_{i}"} for i in range(10)]} + r = _FakeResult(chunks, structured=struct) + out = cgm._extract(r) + assert isinstance(out, list) + assert len(out) == 10 + assert out[9] == {"id": 9, "name": "x_9"} + + +def test_extract_returns_full_list_from_text_chunks_only(): + """If structuredContent is absent but multiple JSON text chunks are + present (older FastMCP behavior), we still want the full list, not + just the first chunk.""" + chunks = [_FakeChunk(json.dumps({"id": i})) for i in range(3)] + r = _FakeResult(chunks, structured=None) + out = cgm._extract(r) + assert out == [{"id": 0}, {"id": 1}, {"id": 2}] + + def test_cli_rejects_unknown_subcommand(capsys): with pytest.raises(SystemExit): cg_mcp.main(["totally_bogus"]) From 403f9587885279b5d8d47684df9cbbb16d131fff Mon Sep 17 00:00:00 2001 From: Dvir Dukhan <12258836+DvirDukhan@users.noreply.github.com> Date: Fri, 29 May 2026 17:10:33 +0300 Subject: [PATCH 63/63] perf(bench/mcp): cap impact_analysis at --limit 50, strip worktree path prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iter1 (chunk-fix, 3125946) un-broke list returns from the MCP layer. That correctness fix exposed two unbounded payload sources in cg-mcp output that re-feed every LLM turn: 1. impact_analysis has no --limit. On a large graph (sympy: 142k edges) a depth=3 traversal routinely returns 500+ nodes. 2. Every node's 'file' is an absolute worktree path (/Users/.../worktrees//sympy/printing/latex.py, ~130 chars). The ~100-char prefix is identical for every entry and contributes nothing actionable. Real impact, observed on sympy__sympy-12481 iter1: - single 'impact_analysis --depth 3' returned 82,041 bytes. - 36 turns × ~82KB re-feed → cost $2.80 (iter0) → $7.28 (iter1). - Accuracy still 10/10, so this is pure context bloat. Fix at the CLI layer (bench/cli/cg_mcp.py) so the MCP server tools stay untouched and other clients (Claude, Cursor) keep their full payloads: - Add '--limit N' (default 50) to impact_analysis subcommand. - _compact_entry strips '//' prefix from 'file', drops empty values, preserves all other fields. - _compact_list applies entry compaction + truncates to limit on every list-returning subcommand (search_code, get_callers, get_callees, get_dependencies, impact_analysis). - find_path: single entry, just strip path. Adds 10 regression tests pinning the new contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bench/cli/cg_mcp.py | 102 ++++++++++- bench/tools/code_graph_mcp/system_preamble.md | 2 +- tests/bench/test_cg_mcp_cli_compaction.py | 167 ++++++++++++++++++ 3 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 tests/bench/test_cg_mcp_cli_compaction.py diff --git a/bench/cli/cg_mcp.py b/bench/cli/cg_mcp.py index 7079c598..a039d6ea 100644 --- a/bench/cli/cg_mcp.py +++ b/bench/cli/cg_mcp.py @@ -40,6 +40,71 @@ from bench.agents import code_graph_mcp_adapter as cgm +# --------------------------------------------------------------------------- +# Output compaction (iter2): keep token budget bounded +# --------------------------------------------------------------------------- +# +# Iter1 (chunk-fix) un-broke list returns from the MCP layer. That exposed +# two unbounded payload sources that re-feed every LLM turn: +# +# 1. `impact_analysis` has no `--limit` — a depth=3 traversal on a +# large project (sympy: 142k edges) routinely returns 500+ nodes. +# 2. Every node's `file` field is an absolute worktree path +# (`/Users/.../worktrees//sympy/printing/latex.py`, +# ~130 chars). The 100+-char prefix is repeated for every entry and +# contributes nothing the agent can act on. +# +# We strip the worktree prefix and cap list outputs at the CLI layer so the +# MCP server tools stay unchanged. Caps default to 50 (matches the other +# tool CLIs) and can be overridden per call. + +# Heuristic worktree-prefix patterns we want to strip from `file` fields. +# We match in order; first hit wins. The "//" segment is added +# dynamically at call time because the project name is per-invocation. +_WORKTREE_FRAGMENTS = ( + "/.worktrees/", # git worktree layout + "/bench/cache/worktrees/", # bench harness layout +) + + +def _strip_worktree_prefix(path: Any, project: str | None) -> Any: + """Convert an absolute file path under a project worktree to repo-relative. + + Returns the input unchanged for non-strings or paths we don't recognize. + """ + if not isinstance(path, str) or not project: + return path + needle = f"/{project}/" + idx = path.find(needle) + if idx < 0: + return path + return path[idx + len(needle):] + + +def _compact_entry(entry: Any, project: str | None) -> Any: + """Drop noise from a single node-summary dict.""" + if not isinstance(entry, dict): + return entry + out: dict[str, Any] = {} + for k, v in entry.items(): + if v in (None, "", [], {}): + continue + if k == "file": + v = _strip_worktree_prefix(v, project) + out[k] = v + return out + + +def _compact_list(items: Any, project: str | None, limit: int | None) -> Any: + """Apply `_compact_entry` + truncate to `limit`.""" + if not isinstance(items, list): + return items + compacted = [_compact_entry(x, project) for x in items] + if limit is not None and limit > 0: + compacted = compacted[:limit] + return compacted + + def _print(obj: Any) -> None: # Compact JSON: agents don't care about indentation, and every byte we # save here is re-fed to the LLM every subsequent turn. @@ -88,6 +153,9 @@ def main(argv: list[str] | None = None) -> int: ia.add_argument("--symbol-id", type=int, required=True, dest="symbol_id") ia.add_argument("--direction", choices=["IN", "OUT"], default="IN") ia.add_argument("--depth", type=int, default=3) + # Iter2: cap output. impact_analysis on large graphs (sympy: 142k edges) + # routinely returns 500+ entries. Default 50 matches the other tools. + ia.add_argument("--limit", type=int, default=50) fp = sub.add_parser("find_path") _add_project(fp) @@ -105,28 +173,46 @@ def main(argv: list[str] | None = None) -> int: cgm.DEFAULT_TIMEOUT_SEC = timeout try: + proj = getattr(args, "project", None) if args.cmd == "index_repo": _print(cgm.index_repo(args.path_or_url, branch=args.branch, ignore=args.ignore)) elif args.cmd == "search_code": - _print(cgm.search_code(args.prefix, args.project, branch=args.branch, limit=args.limit)) + _print(_compact_list( + cgm.search_code(args.prefix, args.project, branch=args.branch, limit=args.limit), + proj, args.limit, + )) elif args.cmd == "get_callers": - _print(cgm.get_callers(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + _print(_compact_list( + cgm.get_callers(args.symbol_id, args.project, branch=args.branch, limit=args.limit), + proj, args.limit, + )) elif args.cmd == "get_callees": - _print(cgm.get_callees(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + _print(_compact_list( + cgm.get_callees(args.symbol_id, args.project, branch=args.branch, limit=args.limit), + proj, args.limit, + )) elif args.cmd == "get_dependencies": - _print(cgm.get_dependencies(args.symbol_id, args.project, branch=args.branch, limit=args.limit)) + _print(_compact_list( + cgm.get_dependencies(args.symbol_id, args.project, branch=args.branch, limit=args.limit), + proj, args.limit, + )) elif args.cmd == "impact_analysis": - _print( + # impact_analysis has no server-side `limit`; cap + compact in CLI. + _print(_compact_list( cgm.impact_analysis( args.symbol_id, args.project, branch=args.branch, direction=args.direction, depth=args.depth, - ) - ) + ), + proj, args.limit, + )) elif args.cmd == "find_path": - _print(cgm.find_path(args.source_id, args.dest_id, args.project, branch=args.branch)) + _print(_compact_entry( + cgm.find_path(args.source_id, args.dest_id, args.project, branch=args.branch), + proj, + )) elif args.cmd == "ask": _print(cgm.ask(args.question, args.project, branch=args.branch)) else: # pragma: no cover — argparse already enforces this diff --git a/bench/tools/code_graph_mcp/system_preamble.md b/bench/tools/code_graph_mcp/system_preamble.md index bf2faf32..b20ae927 100644 --- a/bench/tools/code_graph_mcp/system_preamble.md +++ b/bench/tools/code_graph_mcp/system_preamble.md @@ -25,7 +25,7 @@ A pre-indexed code-graph for this repo is available via the - `cg-mcp get_callers --project P --symbol-id ID [--limit N]` - `cg-mcp get_callees --project P --symbol-id ID [--limit N]` - `cg-mcp get_dependencies --project P --symbol-id ID [--limit N]` -- `cg-mcp impact_analysis --project P --symbol-id ID [--direction IN|OUT] [--depth N]` +- `cg-mcp impact_analysis --project P --symbol-id ID [--direction IN|OUT] [--depth N] [--limit N]` - `cg-mcp find_path --project P --source-id ID --dest-id ID` ## Rules diff --git a/tests/bench/test_cg_mcp_cli_compaction.py b/tests/bench/test_cg_mcp_cli_compaction.py new file mode 100644 index 00000000..cdcccec3 --- /dev/null +++ b/tests/bench/test_cg_mcp_cli_compaction.py @@ -0,0 +1,167 @@ +"""Iter2 regression tests: cg-mcp CLI must compact list outputs. + +Two failure modes from iter1 that this exists to catch: + +1. ``impact_analysis`` returns 500+ entries on large graphs (sympy: 142k + edges). The CLI must apply a default ``--limit 50``. +2. Every returned node carries an absolute worktree path + (``/Users/.../worktrees//sympy/printing/latex.py``). The + 100+-char prefix is identical across entries and contributes nothing + actionable. The CLI must strip everything up to and including + ``//`` so ``file`` is repo-relative. +""" + +from __future__ import annotations + +import io +import json +from contextlib import redirect_stdout +from unittest.mock import patch + +from bench.cli import cg_mcp + + +# ---- helpers -------------------------------------------------------------- + + +def _entry(idx: int) -> dict: + return { + "id": idx, + "name": f"sym_{idx}", + "label": "Function", + "file": ( + "/Users/x/Code/code-graph/.worktrees/bench-combined/" + "bench/cache/worktrees/sympy__sympy-12481__code_graph_mcp/" + f"sympy/printing/latex_{idx}.py" + ), + "line": 100 + idx, + "direction": "IN", + } + + +# ---- unit tests for helpers ---------------------------------------------- + + +def test_strip_worktree_prefix_makes_path_repo_relative(): + p = ( + "/Users/x/Code/code-graph/.worktrees/bench-combined/" + "bench/cache/worktrees/sympy__sympy-12481__code_graph_mcp/" + "sympy/printing/latex.py" + ) + out = cg_mcp._strip_worktree_prefix(p, "sympy__sympy-12481__code_graph_mcp") + assert out == "sympy/printing/latex.py" + + +def test_strip_worktree_prefix_no_op_when_project_missing_from_path(): + p = "/tmp/somewhere/else.py" + out = cg_mcp._strip_worktree_prefix(p, "sympy__sympy-12481__code_graph_mcp") + # No matching project segment, leave untouched. + assert out == p + + +def test_strip_worktree_prefix_handles_none_project(): + p = "/abs/path.py" + assert cg_mcp._strip_worktree_prefix(p, None) == p + + +def test_compact_entry_drops_empty_fields_and_strips_path(): + e = _entry(7) + e["empty_list"] = [] + e["empty_str"] = "" + e["none_field"] = None + out = cg_mcp._compact_entry(e, "sympy__sympy-12481__code_graph_mcp") + assert out["file"] == "sympy/printing/latex_7.py" + # empties dropped + assert "empty_list" not in out + assert "empty_str" not in out + assert "none_field" not in out + # kept fields intact + assert out["id"] == 7 + assert out["name"] == "sym_7" + + +def test_compact_list_caps_at_limit_and_strips_paths(): + items = [_entry(i) for i in range(200)] + out = cg_mcp._compact_list(items, "sympy__sympy-12481__code_graph_mcp", 50) + assert len(out) == 50 + assert out[0]["file"] == "sympy/printing/latex_0.py" + assert out[49]["file"] == "sympy/printing/latex_49.py" + + +def test_compact_list_no_limit_keeps_all_but_still_strips(): + items = [_entry(i) for i in range(3)] + out = cg_mcp._compact_list(items, "sympy__sympy-12481__code_graph_mcp", None) + assert len(out) == 3 + for o in out: + assert "worktrees" not in o["file"] + + +# ---- CLI integration ------------------------------------------------------ + + +def _run_cli(argv: list[str]) -> dict | list: + buf = io.StringIO() + with redirect_stdout(buf): + rc = cg_mcp.main(argv) + assert rc == 0, f"CLI returned {rc}; stdout={buf.getvalue()!r}" + return json.loads(buf.getvalue()) + + +def test_cli_impact_analysis_applies_default_limit_50(): + # Adapter returns the raw list (already chunk-fix-corrected); CLI must cap. + fake_response = [_entry(i) for i in range(120)] + with patch.object(cg_mcp.cgm, "impact_analysis", return_value=fake_response): + out = _run_cli([ + "impact_analysis", + "--project", "sympy__sympy-12481__code_graph_mcp", + "--symbol-id", "7417", + ]) + assert isinstance(out, list) + assert len(out) == 50, f"expected default limit 50, got {len(out)}" + # Path stripped + assert out[0]["file"] == "sympy/printing/latex_0.py" + + +def test_cli_impact_analysis_respects_explicit_limit(): + fake_response = [_entry(i) for i in range(120)] + with patch.object(cg_mcp.cgm, "impact_analysis", return_value=fake_response): + out = _run_cli([ + "impact_analysis", + "--project", "sympy__sympy-12481__code_graph_mcp", + "--symbol-id", "7417", + "--limit", "10", + ]) + assert len(out) == 10 + + +def test_cli_get_callers_strips_paths(): + fake_response = [_entry(i) for i in range(5)] + with patch.object(cg_mcp.cgm, "get_callers", return_value=fake_response): + out = _run_cli([ + "get_callers", + "--project", "sympy__sympy-12481__code_graph_mcp", + "--symbol-id", "42", + ]) + assert all("worktrees" not in e["file"] for e in out) + + +def test_cli_payload_size_shrinks_substantially(): + """End-to-end smoke: 200 entries through impact_analysis should be + much smaller than the raw chunk-fix output (80KB+ on real sympy data).""" + fake_response = [_entry(i) for i in range(200)] + raw = json.dumps(fake_response, separators=(",", ":")) + raw_size = len(raw) + + buf = io.StringIO() + with redirect_stdout(buf), patch.object(cg_mcp.cgm, "impact_analysis", return_value=fake_response): + cg_mcp.main([ + "impact_analysis", + "--project", "sympy__sympy-12481__code_graph_mcp", + "--symbol-id", "7417", + ]) + capped_size = len(buf.getvalue()) + + # Default cap (50) + path strip should cut output to a small fraction. + assert capped_size < raw_size * 0.30, ( + f"compaction insufficient: raw={raw_size}B capped={capped_size}B" + )