diff --git a/.github/workflows/nightly-llm-ab.yml b/.github/workflows/nightly-llm-ab.yml new file mode 100644 index 0000000..9d0dab5 --- /dev/null +++ b/.github/workflows/nightly-llm-ab.yml @@ -0,0 +1,71 @@ +name: Nightly LLM A/B + +# Optional, NON-gating answer-quality A/B (knowbase context vs RAG context), judged by an LLM. +# Runs nightly and on demand. Self-skips when no API key secret is set; never blocks merges (it is a +# separate workflow from CI, and the judge step is continue-on-error). The deterministic Tier-3 recall +# gate in CI remains the hard floor. + +on: + schedule: + - cron: "0 6 * * *" # 06:00 UTC nightly + workflow_dispatch: + +concurrency: + group: nightly-llm-ab + cancel-in-progress: true + +jobs: + ab: + name: LLM-judged knowledge-vs-RAG A/B + runs-on: ubuntu-latest + services: + postgres: + image: pgvector/pgvector:pg17 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + KB_TEST_DB_URL: postgresql+psycopg://postgres:postgres@127.0.0.1:5432/postgres + KB_LLM_PROVIDER: anthropic + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + KB_LLM_AB_METRICS: tier3_llm_ab_metrics.json + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.12" + enable-cache: true + + - name: Install dependencies (incl. llm extra) + run: uv sync --extra dev --extra embed --extra llm + + - name: Cache embedding model + uses: actions/cache@v4 + with: + path: ~/.cache/huggingface + key: hf-all-MiniLM-L6-v2-v1 + + - name: Warm up embedding model + run: uv run python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')" + + - name: LLM-judged A/B (tracked, non-gating; self-skips without ANTHROPIC_API_KEY) + continue-on-error: true + run: uv run pytest src/kb/eval/tier3_llm_judge_test.py -q -s + + - name: Upload A/B metrics + uses: actions/upload-artifact@v4 + with: + name: tier3-llm-ab-metrics + path: tier3_llm_ab_metrics.json + if-no-files-found: ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index 975ac77..5afb98a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Tier-3 entity questions** (`kb.eval.questions`): the knowledge-vs-RAG A/B now also covers domain entities (a two-file `Order`/`LineItem` fixture), asserting knowbase cross-file recall@k == 1.0 for entity questions as well as API-contract questions. +- **Nightly LLM-judged A/B** (`kb.llm`, `kb.eval.tier3_llm_judge_test`, `.github/workflows/nightly-llm-ab.yml`): + an optional, key-gated, **non-gating** answer-quality comparison. An answerer LLM answers each question + from knowbase-grounded context vs RAG-over-source context; a judge LLM scores accuracy against + hand-written `GOLD` references and flags hallucination (claims unsupported by that arm's context). + `kb.llm.providers` mirrors the embed-provider pattern (Anthropic default, OpenAI optional, lazy imports + via the new `llm` extra); the test self-skips without an API key and asserts only that the A/B ran + (never the win); the nightly workflow uploads a metrics artifact. `RagHit` gained a `raw_text` field so + the RAG arm can feed chunk text to the answerer. ## [0.2.0] - 2026-06-02 diff --git a/DESIGN.md b/DESIGN.md index 329b88d..4063dd6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -264,10 +264,14 @@ Eval is **co-equal with extraction**, weighted to cheap/exact tiers that gate CI - **Tier 2 — golden curated repos (TRACKED, non-gating).** 3–5 SHA-pinned permissive Python repos; one **held out** and never used for tuning (the real trust signal). Report per-repo, never just the mean. -- **Tier 3 — downstream vs RAG (TRACKED, after MCP).** Fixed ~30–50 question set; coding agent +- **Tier 3 — downstream vs RAG (TRACKED, after MCP).** Fixed question set; coding agent answers with knowbase-MCP vs a **frozen, peer-reviewed** pgvector-RAG baseline (same Postgres, same model). **Pre-register the win threshold.** Metrics: grounded-answer accuracy, hallucination rate (claims with no provenance), tokens-to-answer, tool round-trips. + *Implemented:* the deterministic cross-file recall gate (`tier3_rag_test`, HARD) plus an optional, + key-gated, NON-gating **LLM-judged A/B** (`kb.llm` + `tier3_llm_judge_test`, run nightly): an answerer + answers each question from knowbase context vs RAG context, and a judge scores accuracy against + hand-written gold + hallucination, comparing to a pre-registered threshold (printed, never asserted). **Invariants asserted as exact ground truth every run:** every artifact has ≥1 `derived_from` row (zero orphans); re-running an extractor on the same span identity+version yields an diff --git a/README.md b/README.md index f38359f..dae5be9 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,9 @@ flowchart LR - **A frozen RAG-over-source baseline** and the **Tier-3 knowledge-vs-RAG recall gate** — the honest A/B that backs the "knowledge > RAG" thesis. - **Eight HARD CI eval gates** (see [Development](#development)). -**Not done yet** (and deliberately not faked): the semantic / **LLM-grounded** extraction layer, the nightly LLM-judged A/B, ADR mining from git history, grounded business-process extraction, incremental re-index on git push, and languages beyond Python. See the [Roadmap](#roadmap). +- **A nightly LLM-judged A/B** (optional, key-gated, **non-gating**) — an answerer LLM answers each question from knowbase's grounded context vs a RAG-over-source context, and a judge LLM scores **answer accuracy** (against hand-written gold) + **hallucination**. Tracked metrics on top of recall; it never blocks CI. + +**Not done yet** (and deliberately not faked): the semantic / **LLM-grounded** extraction layer, ADR mining from git history, grounded business-process extraction, incremental re-index on git push, and languages beyond Python. See the [Roadmap](#roadmap). ## Quickstart @@ -226,7 +228,7 @@ flowchart LR Next milestones: -- [ ] **Nightly LLM-judged A/B** (key-gated, non-gating) — grounded-answer accuracy + hallucination rate on top of recall. +- [x] **Nightly LLM-judged A/B** (key-gated, non-gating) — grounded-answer accuracy + hallucination rate on top of recall. *(shipped)* - [ ] **LLM-grounded semantic layer** — model-backed artifacts that still carry ≥ 1 span (`extraction_method = "llm_grounded"`). - [ ] **Incremental re-index on git push** — turn the diff-based invalidation seed into live updates. - [ ] **ADR mining** from git / PR history. diff --git a/pyproject.toml b/pyproject.toml index f42d4f6..27b782e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,13 @@ dev = [ embed = [ "sentence-transformers>=3,<6", ] +# LLM backend for the optional, nightly, NON-gating LLM-judged A/B (kb.llm + tier3_llm_judge_test). +# Not needed for index/serve/embed; the nightly workflow uses `uv sync --extra dev --extra embed +# --extra llm`. Also makes the existing OpenAI embedding adapter installable. +llm = [ + "anthropic>=0.40", + "openai>=1.40", +] [project.scripts] kb = "kb.daemon.cli:app" @@ -86,7 +93,7 @@ ignore_errors = true [[tool.mypy.overrides]] module = [ "pygit2.*", "grimp.*", "sqlparse.*", "fastapi.*", "starlette.*", "fastmcp.*", "mcp.*", - "sentence_transformers.*", "pgvector.*", "openai.*", + "sentence_transformers.*", "pgvector.*", "openai.*", "anthropic.*", ] ignore_missing_imports = true diff --git a/src/kb/eval/questions.py b/src/kb/eval/questions.py index 532414b..1ac7218 100644 --- a/src/kb/eval/questions.py +++ b/src/kb/eval/questions.py @@ -75,3 +75,24 @@ class Question: Question("e3", "Which model does the Order entity's items field reference, and where is it?", ENTITY_CROSS_FILE, frozenset({"entity:app.domain.order.Order"})), ] + +# Hand-written reference answers (the gold oracle for the nightly LLM-judged A/B, PR-3b). Derived +# from the fixtures above (we own them), so accuracy is judged against ground truth — not against +# either arm's own retrieval (no knowbase-favoring bias). Keep in sync with FILES / ENTITY_FILES. +GOLD: dict[str, str] = { + "q1": "A JSON array of OrderOut objects; each OrderOut has id (int) and total (float).", + "q2": "The fields of OrderOut: id (int) and total (float).", + "q3": "Accepts an OrderIn request body with a single field item (str); returns an OrderOut " + "(id: int, total: float).", + "q4": "OrderOut, which has id (int) and total (float).", + "q5": "OrderOut (id: int, total: float).", + "q6": "201.", + "q7": "float.", + "q8": "GET /api/orders (also POST and GET /api/orders/{order_id}) returns OrderOut; " + "OrderOut is defined in src/app/schemas.py.", + "e1": "Order (a dataclass) has id (int) and items (a list of LineItem); each LineItem has " + "sku (str) and qty (int, default 1).", + "e2": "Order has id (int) and items, where items is list[LineItem]; LineItem has sku (str) and " + "qty (int).", + "e3": "It references LineItem, defined in src/app/domain/line_item.py.", +} diff --git a/src/kb/eval/tier3_llm_judge_test.py b/src/kb/eval/tier3_llm_judge_test.py new file mode 100644 index 0000000..9fe6ee6 --- /dev/null +++ b/src/kb/eval/tier3_llm_judge_test.py @@ -0,0 +1,175 @@ +"""TRACKED (NON-gating) — Tier 3: LLM-judged answer quality, knowbase context vs RAG context (§9). + +Beyond Tier-3 *recall* (tier3_rag_test), this measures *answer quality*: an answerer LLM answers +each question from the knowbase-grounded context and, separately, from the RAG-over-source context. +A judge LLM scores each answer against a hand-written GOLD reference for **accuracy**, and flags +**hallucination** (a claim unsupported by that arm's own provided context). + +This is **nightly, key-gated, and NON-gating**: it self-skips without an API key, asserts only that +the A/B actually ran (never the win), and writes a metrics JSON for the CI artifact. The +deterministic Tier-3 recall gate remains the hard floor. Self-judging bias is accepted here (see +DESIGN §9); a distinct judge model can be set via KB_LLM_JUDGE_MODEL. +""" + +from __future__ import annotations + +import json +import os +import re + +import pytest +from sqlalchemy import Engine + +from kb.daemon.pipeline import index_commit +from kb.embed.population import embed_snapshot +from kb.eval._fixtures import make_git_repo +from kb.eval.questions import ENTITY_FILES, GOLD, QUESTIONS +from kb.eval.tier1_api_test import FILES +from kb.extract.deterministic.entities import EntityExtractor +from kb.extract.deterministic.fastapi_contract import FastAPIExtractor +from kb.llm.providers import default_llm_provider, has_llm_key +from kb.mcp.records import summarize +from kb.rag.baseline import index_rag_baseline, rag_retrieve +from kb.store import queries as q +from kb.store.queries import provenance_for_artifact + +pytestmark = pytest.mark.skipif( + not has_llm_key(), reason="no LLM API key (set ANTHROPIC_API_KEY or OPENAI_API_KEY)" +) + +K = 5 +WIN_ACCURACY_MARGIN = 0.15 # pre-registered: knowbase wins iff acc margin >= this AND hall <= RAG's + +ANSWER_SYSTEM = ( + "Answer the question using ONLY the provided context about a codebase. Be concise and " + "specific. If the context does not contain the answer, say you cannot tell from the context." +) +JUDGE_SYSTEM = "You are a strict grader. Respond with ONE JSON object and nothing else." + + +@pytest.fixture(scope="module") +def prepared(engine: Engine, tmp_path_factory, st_provider) -> tuple[Engine, str]: + repo = tmp_path_factory.mktemp("tier3_llm") + sha = make_git_repo(repo, [{**FILES, **ENTITY_FILES}])[0] + index_commit( + engine, + str(repo), + sha, + extractors=[FastAPIExtractor(), EntityExtractor()], + first_party_root="src", + ) + embed_snapshot(engine, sha, st_provider) + index_rag_baseline(engine, str(repo), sha, st_provider) + return engine, sha + + +def _knowbase_context(conn, sha, question, st_provider) -> str: + qvec = st_provider.embed([question])[0] + blocks = [] + for row in q.similar_artifacts_by_embedding(conn, sha, qvec, K): + prov = provenance_for_artifact(conn, sha, row.logical_key) + prov_str = ", ".join(f"{p.file_path}:{p.start_line}" for p in prov) + blocks.append( + f"[{row.logical_key}] kind={row.kind}\n" + f"summary: {summarize(row.kind, row.payload)}\n" + f"details: {json.dumps(row.payload, default=str)[:600]}\n" + f"provenance: {prov_str}" + ) + return "\n\n".join(blocks) if blocks else "(no knowledge units found)" + + +def _rag_context(conn, sha, question, st_provider) -> str: + hits = rag_retrieve(conn, question, st_provider, sha, K) + blocks = [f"# {h.file_path}:{h.start_line}-{h.end_line}\n{h.raw_text}" for h in hits] + return "\n\n".join(blocks) if blocks else "(no source chunks found)" + + +def _answer(provider, question: str, context: str) -> str: + return provider.complete(ANSWER_SYSTEM, f"Context:\n{context}\n\nQuestion: {question}") + + +def _judge(provider, question: str, gold: str, answer: str, context: str) -> dict: + prompt = ( + f"Question: {question}\n" + f"Gold answer: {gold}\n" + f"Candidate answer: {answer}\n\n" + f"Candidate's source context:\n{context}\n\n" + 'Return JSON {"accuracy": 0|1, "hallucinated": 0|1, "note": "..."} where accuracy=1 iff ' + "the candidate conveys the gold answer's key facts (paraphrase is fine), and " + "hallucinated=1 iff the candidate states a fact not supported by its source context." + ) + return _parse_verdict(provider.complete(JUDGE_SYSTEM, prompt, max_tokens=300)) + + +def _parse_verdict(raw: str) -> dict: + match = re.search(r"\{.*\}", raw, re.S) + if match: + try: + data = json.loads(match.group(0)) + return { + "accuracy": int(bool(data.get("accuracy"))), + "hallucinated": int(bool(data.get("hallucinated"))), + "note": str(data.get("note", ""))[:200], + "parse_error": False, + } + except json.JSONDecodeError: + pass + return {"accuracy": 0, "hallucinated": 0, "note": "unparseable", "parse_error": True} + + +def test_llm_judged_ab(prepared: tuple[Engine, str], st_provider) -> None: + engine, sha = prepared + answerer = default_llm_provider() + judge = default_llm_provider(os.environ.get("KB_LLM_JUDGE_MODEL")) + + records = [] + with engine.connect() as conn: + for question in QUESTIONS: + gold = GOLD[question.id] + kb_ctx = _knowbase_context(conn, sha, question.question, st_provider) + rag_ctx = _rag_context(conn, sha, question.question, st_provider) + kb_ans = _answer(answerer, question.question, kb_ctx) + rag_ans = _answer(answerer, question.question, rag_ctx) + kb_v = _judge(judge, question.question, gold, kb_ans, kb_ctx) + rag_v = _judge(judge, question.question, gold, rag_ans, rag_ctx) + records.append({"id": question.id, "knowbase": kb_v, "rag": rag_v}) + + n = len(records) + + def mean(arm: str, key: str) -> float: + return sum(r[arm][key] for r in records) / n + + kb_acc, rag_acc = mean("knowbase", "accuracy"), mean("rag", "accuracy") + kb_hall, rag_hall = mean("knowbase", "hallucinated"), mean("rag", "hallucinated") + win = (kb_acc - rag_acc >= WIN_ACCURACY_MARGIN) and (kb_hall <= rag_hall) + + summary = { + "answerer": answerer.model_id, + "judge": judge.model_id, + "n": n, + "k": K, + "knowbase": {"accuracy": kb_acc, "hallucination": kb_hall}, + "rag": {"accuracy": rag_acc, "hallucination": rag_hall}, + "pre_registered_threshold": { + "accuracy_margin": WIN_ACCURACY_MARGIN, + "hallucination": "knowbase <= rag", + }, + "win": win, + "records": records, + } + out_path = os.environ.get("KB_LLM_AB_METRICS", "tier3_llm_ab_metrics.json") + with open(out_path, "w", encoding="utf-8") as fh: + json.dump(summary, fh, indent=2) + + print( + f"\n[tier3-llm] answerer={answerer.model_id} judge={judge.model_id} " + f"n={n} (TRACKED, non-gating)\n" + f" accuracy: knowbase={kb_acc:.3f} RAG={rag_acc:.3f}\n" + f" hallucination: knowbase={kb_hall:.3f} RAG={rag_hall:.3f}\n" + f" pre-registered win (acc margin >= {WIN_ACCURACY_MARGIN} and hall <= RAG): " + f"{'PASS' if win else 'not met'} -> {out_path}" + ) + + # NON-gating: assert only that the A/B actually ran for every question — never the win. + assert n == len(QUESTIONS) + assert all(r["knowbase"]["note"] is not None for r in records) diff --git a/src/kb/llm/__init__.py b/src/kb/llm/__init__.py new file mode 100644 index 0000000..aede367 --- /dev/null +++ b/src/kb/llm/__init__.py @@ -0,0 +1,6 @@ +"""Replaceable LLM adapters for the optional, nightly, NON-gating LLM-judged A/B (DESIGN.md §1, §9). + +Used only by ``kb.eval.tier3_llm_judge_test``; never on the index or serve path. Heavy SDK imports +(anthropic / openai) are lazy so importing this package is cheap and collection-safe even when those +packages are not installed. +""" diff --git a/src/kb/llm/providers.py b/src/kb/llm/providers.py new file mode 100644 index 0000000..22f9850 --- /dev/null +++ b/src/kb/llm/providers.py @@ -0,0 +1,86 @@ +"""LLM providers behind a thin adapter — answerer + judge for the nightly A/B (DESIGN.md §1, §9). + +Mirrors ``kb.embed.providers``: a ``Protocol`` + lazy heavy imports + ``default_llm_provider()`` via +env. Anthropic is the default; OpenAI is an optional alternative. Nothing here runs on the index or +serve path — only the optional, key-gated, NON-gating ``tier3_llm_judge_test`` uses it. SDK imports +are lazy so this module imports cleanly (and the judge test collects + skips) without the packages. +""" + +from __future__ import annotations + +import os +from typing import Protocol, runtime_checkable + +DEFAULT_ANTHROPIC_MODEL = "claude-opus-4-8" +DEFAULT_OPENAI_MODEL = "gpt-4o-mini" + + +@runtime_checkable +class LLMProvider(Protocol): + model_id: str + + def complete(self, system: str, user: str, *, max_tokens: int = 1024) -> str: ... + + +class AnthropicProvider: + """Anthropic Messages API adapter (default). Reads ``ANTHROPIC_API_KEY``.""" + + def __init__(self, model: str = DEFAULT_ANTHROPIC_MODEL) -> None: + from anthropic import Anthropic # lazy: keeps the SDK off the import/serve path + + self._client = Anthropic() + self._model = model + self.model_id = f"anthropic:{model}" + + def complete(self, system: str, user: str, *, max_tokens: int = 1024) -> str: + message = self._client.messages.create( + model=self._model, + max_tokens=max_tokens, + system=system, + messages=[{"role": "user", "content": user}], + ) + return "".join(block.text for block in message.content if block.type == "text") + + +class OpenAIChatProvider: + """OpenAI Chat Completions adapter (optional). Reads ``OPENAI_API_KEY``.""" + + def __init__(self, model: str = DEFAULT_OPENAI_MODEL) -> None: + from openai import OpenAI # lazy + + self._client = OpenAI() + self._model = model + self.model_id = f"openai:{model}" + + def complete(self, system: str, user: str, *, max_tokens: int = 1024) -> str: + resp = self._client.chat.completions.create( + model=self._model, + max_tokens=max_tokens, + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": user}, + ], + ) + return resp.choices[0].message.content or "" + + +def _provider_name() -> str: + return os.environ.get("KB_LLM_PROVIDER", "anthropic").lower() + + +def default_llm_provider(model: str | None = None) -> LLMProvider: + """Select the provider via ``KB_LLM_PROVIDER`` in {"anthropic","openai"}; default "anthropic". + + ``model`` (or ``KB_LLM_MODEL``) overrides the provider's default model. + """ + chosen = model or os.environ.get("KB_LLM_MODEL") + if _provider_name() == "openai": + return OpenAIChatProvider(chosen or DEFAULT_OPENAI_MODEL) + return AnthropicProvider(chosen or DEFAULT_ANTHROPIC_MODEL) + + +def has_llm_key() -> bool: + """Whether the selected provider's API key is present (drives the judge test's ``skipif``).""" + if _provider_name() == "openai": + return bool(os.environ.get("OPENAI_API_KEY")) + return bool(os.environ.get("ANTHROPIC_API_KEY")) diff --git a/src/kb/rag/baseline.py b/src/kb/rag/baseline.py index 734d577..9f45a91 100644 --- a/src/kb/rag/baseline.py +++ b/src/kb/rag/baseline.py @@ -39,6 +39,7 @@ class RagHit: start_line: int end_line: int distance: float + raw_text: str = "" # the chunk's source text (for the LLM A/B answerer); empty unless selected def chunk_source(file_path: str, source: str) -> list[Chunk]: @@ -103,11 +104,13 @@ def rag_retrieve( m.rag_chunk.c.start_line, m.rag_chunk.c.end_line, distance.label("distance"), + m.rag_chunk.c.raw_text, ) .where(m.rag_chunk.c.sha == sha, m.rag_chunk.c.embedding.is_not(None)) .order_by(distance) .limit(k) ).all() return [ - RagHit(r.file_path, r.chunk_idx, r.start_line, r.end_line, float(r.distance)) for r in rows + RagHit(r.file_path, r.chunk_idx, r.start_line, r.end_line, float(r.distance), r.raw_text) + for r in rows ] diff --git a/uv.lock b/uv.lock index 7d7b6e7..3c44300 100644 --- a/uv.lock +++ b/uv.lock @@ -50,6 +50,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.111.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/8a/9afc7305a2ce4b52b30e137f83cd2a6a90b918b3997073db11bb5a1de55a/anthropic-0.111.0.tar.gz", hash = "sha256:39cbda0ac17a6d423e5bf609811bd69b26eddf6299d7a468126e05bc711ce826", size = 934001, upload-time = "2026-06-18T17:31:44.733Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/bb/09e82a81885d787f350fb55ca9df865b63140dd28b3b5b3104c4ae261657/anthropic-0.111.0-py3-none-any.whl", hash = "sha256:c14edb36ed80da9099acbd26b5cec810d76606c31f32a0d56a4cf9d4fa9e25ae", size = 929774, upload-time = "2026-06-18T17:31:43.116Z" }, +] + [[package]] name = "anyio" version = "4.13.0" @@ -385,6 +404,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/8d/7f362c2fb8ef4decd2160bc24d4292c6ca658cc6d9a161b89ca5122bbdbf/cyclopts-4.16.1-py3-none-any.whl", hash = "sha256:617795392c4113a2c2cc7af716f20244900e87f23daa05442d1268d81472a592", size = 219020, upload-time = "2026-05-25T15:29:09.646Z" }, ] +[[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 = "dnspython" version = "2.8.0" @@ -825,6 +853,78 @@ 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/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { 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 = "joblib" version = "1.5.3" @@ -945,14 +1045,20 @@ dev = [ embed = [ { name = "sentence-transformers" }, ] +llm = [ + { name = "anthropic" }, + { name = "openai" }, +] [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.13" }, + { name = "anthropic", marker = "extra == 'llm'", specifier = ">=0.40" }, { name = "fastapi", marker = "extra == 'dev'", specifier = ">=0.110" }, { name = "fastmcp", specifier = ">=3.3,<4" }, { name = "grimp", specifier = ">=3,<4" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, + { name = "openai", marker = "extra == 'llm'", specifier = ">=1.40" }, { name = "pgvector", specifier = ">=0.4,<0.5" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.1,<4" }, { name = "pydantic", specifier = ">=2.6,<3" }, @@ -968,7 +1074,7 @@ requires-dist = [ { name = "tree-sitter-python", specifier = ">=0.25.0" }, { name = "typer", specifier = ">=0.12" }, ] -provides-extras = ["dev", "embed"] +provides-extras = ["dev", "embed", "llm"] [[package]] name = "librt" @@ -1453,6 +1559,25 @@ wheels = [ { 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 = "openai" +version = "2.43.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/f3/fa/88d0c58a0c58df7e6758e66b99c5d028d5e0bb49f8812d7203940cd9dbf1/openai-2.43.0.tar.gz", hash = "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017", size = 785369, upload-time = "2026-06-17T17:06:56.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" }, +] + [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -2371,6 +2496,15 @@ 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" }, ] +[[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 = "sqlalchemy" version = "2.0.50"