aid-runtime-python 0.1.39__tar.gz → 0.1.51__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. aid_runtime_python-0.1.51/PKG-INFO +276 -0
  2. aid_runtime_python-0.1.51/README.md +242 -0
  3. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/pyproject.toml +1 -1
  4. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/__init__.py +1 -1
  5. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/gate.py +1 -1
  6. aid_runtime_python-0.1.51/src/aid_runtime_python.egg-info/PKG-INFO +276 -0
  7. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_metrics.py +1 -1
  8. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_tracing.py +3 -0
  9. aid_runtime_python-0.1.39/PKG-INFO +0 -268
  10. aid_runtime_python-0.1.39/README.md +0 -234
  11. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/PKG-INFO +0 -268
  12. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/setup.cfg +0 -0
  13. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/__init__.py +0 -0
  14. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/chat_model.py +0 -0
  15. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/deep_agents.py +0 -0
  16. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/engine.py +0 -0
  17. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/guardrails.py +0 -0
  18. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/ir.py +0 -0
  19. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/loop.py +0 -0
  20. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/memory.py +0 -0
  21. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/policy.py +0 -0
  22. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/agents/state.py +0 -0
  23. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/cache.py +0 -0
  24. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/concurrency.py +0 -0
  25. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/control.py +0 -0
  26. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/cost.py +0 -0
  27. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/errors.py +0 -0
  28. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/__init__.py +0 -0
  29. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/dataset.py +0 -0
  30. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/gate.py +0 -0
  31. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/metrics.py +0 -0
  32. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/predicate.py +0 -0
  33. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/evals/runner.py +0 -0
  34. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/gateway.py +0 -0
  35. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/pipeline.py +0 -0
  36. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/prompts.py +0 -0
  37. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/providers/__init__.py +0 -0
  38. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/providers/base.py +0 -0
  39. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/providers/fake.py +0 -0
  40. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/providers/http.py +0 -0
  41. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/py.typed +0 -0
  42. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/__init__.py +0 -0
  43. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/chunking.py +0 -0
  44. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/citations.py +0 -0
  45. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/connectors.py +0 -0
  46. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/embedding.py +0 -0
  47. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/hybrid.py +0 -0
  48. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/metrics.py +0 -0
  49. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/pgvector.py +0 -0
  50. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/rerank.py +0 -0
  51. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/retriever.py +0 -0
  52. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/stores.py +0 -0
  53. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/retrieval/types.py +0 -0
  54. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/steps.py +0 -0
  55. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/structured.py +0 -0
  56. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/tools.py +0 -0
  57. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/tracing.py +0 -0
  58. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime/types.py +0 -0
  59. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime_python.egg-info/SOURCES.txt +0 -0
  60. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime_python.egg-info/dependency_links.txt +0 -0
  61. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime_python.egg-info/requires.txt +0 -0
  62. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/src/aid_runtime_python.egg-info/top_level.txt +0 -0
  63. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_agents_budget.py +0 -0
  64. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_agents_deep_agents.py +0 -0
  65. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_agents_hitl.py +0 -0
  66. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_agents_loop.py +0 -0
  67. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_agents_memory.py +0 -0
  68. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_cache.py +0 -0
  69. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_concurrency.py +0 -0
  70. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_control.py +0 -0
  71. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_cost.py +0 -0
  72. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_dataset.py +0 -0
  73. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_evals.py +0 -0
  74. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_evals_assert_gate.py +0 -0
  75. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_evals_gate.py +0 -0
  76. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_evals_predicate.py +0 -0
  77. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_gateway.py +0 -0
  78. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_guardrails.py +0 -0
  79. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_http_provider.py +0 -0
  80. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_package.py +0 -0
  81. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_pipeline.py +0 -0
  82. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_prompts.py +0 -0
  83. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_chunking.py +0 -0
  84. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_citations.py +0 -0
  85. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_connectors.py +0 -0
  86. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_embedding.py +0 -0
  87. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_gate.py +0 -0
  88. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_hybrid.py +0 -0
  89. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_rerank.py +0 -0
  90. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_stores.py +0 -0
  91. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_retrieval_types.py +0 -0
  92. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_steps.py +0 -0
  93. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_streaming.py +0 -0
  94. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_structured.py +0 -0
  95. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_tool_permissions.py +0 -0
  96. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_tools.py +0 -0
  97. {aid_runtime_python-0.1.39 → aid_runtime_python-0.1.51}/tests/test_types.py +0 -0
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: aid-runtime-python
3
+ Version: 0.1.51
4
+ Summary: The AID Framework Python runtime: model gateway with pinning and cost budgets, prompt registry, tool executor, OTel GenAI tracing, plain-async pipeline, and an eval harness that gates on statistical regression against a stored baseline.
5
+ Author: The AID Framework authors
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/aid-framework/aid-framework
8
+ Project-URL: Documentation, https://github.com/aid-framework/aid-framework/blob/main/docs/design.md
9
+ Project-URL: Issues, https://github.com/aid-framework/aid-framework/issues
10
+ Keywords: ai,codegen,llm,evals,opentelemetry,pydantic
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: httpx<1,>=0.27
21
+ Requires-Dist: opentelemetry-api<2,>=1.24
22
+ Requires-Dist: pydantic<3,>=2.7
23
+ Requires-Dist: pydantic-ai-slim<3,>=2
24
+ Provides-Extra: pgvector
25
+ Requires-Dist: psycopg[binary]<4,>=3.1; extra == "pgvector"
26
+ Provides-Extra: deepagents
27
+ Requires-Dist: deepagents==0.7.32; extra == "deepagents"
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy<3,>=1.11; extra == "dev"
30
+ Requires-Dist: opentelemetry-sdk<2,>=1.24; extra == "dev"
31
+ Requires-Dist: pytest<10,>=8; extra == "dev"
32
+ Requires-Dist: pytest-asyncio<2,>=0.24; extra == "dev"
33
+ Requires-Dist: ruff<1,>=0.6; extra == "dev"
34
+
35
+ # `aid-runtime-python`
36
+
37
+ The Python runtime for [AID Framework](https://github.com/aid-framework/aid-framework): the behavior layer that
38
+ apps generated by the [`py-fastapi`](https://github.com/aid-framework/aid-framework/tree/main/packages/generators/py-fastapi)
39
+ target import. Generated code is thin glue; this is where the behavior lives.
40
+
41
+ Distribution name **`aid-runtime-python`**, import package **`aid_runtime`**. The two differ deliberately: a
42
+ distribution name is conventionally hyphenated, and a Python import package cannot be. That asymmetry is asserted
43
+ by the test suite so it cannot drift.
44
+
45
+ ## Install
46
+
47
+ ```sh
48
+ pip install aid-runtime-python
49
+ ```
50
+
51
+ Requires Python 3.11 or newer. Dependencies are `httpx`, `pydantic`, `pydantic-ai-slim` and
52
+ `opentelemetry-api`.
53
+
54
+ Two optional extras:
55
+
56
+ ```sh
57
+ pip install "aid-runtime-python[pgvector]" # the pgvector store adapter
58
+ pip install "aid-runtime-python[deepagents]" # the deepagents agent engine, pinned
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```python
64
+ from aid_runtime import (
65
+ Budget,
66
+ CompletionRequest,
67
+ CostProfile,
68
+ ModelGateway,
69
+ ModelSpec,
70
+ OpenAICompatibleProvider,
71
+ PinnedModel,
72
+ )
73
+
74
+ spec = ModelSpec(
75
+ name="gpt-4o-2024-08-06",
76
+ provider="openai-compatible",
77
+ cost=CostProfile(input_per_1k_usd=0.0025, output_per_1k_usd=0.01),
78
+ context_window=128_000,
79
+ max_output_tokens=4_096,
80
+ )
81
+
82
+ gateway = ModelGateway(
83
+ providers={"openai-compatible": OpenAICompatibleProvider(api_key=...)},
84
+ aliases={"small": PinnedModel(alias="small", spec=spec)},
85
+ budget=Budget(max_usd=5.0, scope="nightly"),
86
+ )
87
+
88
+ result = await gateway.generate("small", CompletionRequest.prompt("hello"))
89
+
90
+ result.text, result.usage, result.cost_usd, result.alias, result.attempts, result.fell_back
91
+ ```
92
+
93
+ ## What it provides
94
+
95
+ | Module | Provides |
96
+ | --- | --- |
97
+ | `gateway.py` | `ModelGateway` — alias resolution, capability negotiation, ordered fallback, cost accounting, GenAI spans |
98
+ | `providers/` | The `Provider` protocol, one HTTP adapter (`OpenAICompatibleProvider`), one deterministic fake (`FakeProvider`) |
99
+ | `prompts.py` | `PromptTemplate` / `PromptRegistry` — versioned prompts, typed rendering, few-shot assembly |
100
+ | `structured.py` | `build_agent` — structured output via Pydantic AI only |
101
+ | `tools.py` | `Tool` / `ToolExecutor` — declared definitions, argument validation, structured results |
102
+ | `pipeline.py` | `Pipeline` / `PipelineContext` — plain-async named steps over a frozen context |
103
+ | `agents/` | `AgentRuntime` — the loop, transfer vs. delegate, step/token/cost budgets with bounded degrade, memory, checkpointed human-in-the-loop |
104
+ | `retrieval/` | Connectors, chunkers, the `Embedder` / `Reranker` / `VectorStore` protocols, hybrid fusion, citations, retrieval metrics |
105
+ | `evals/` | Datasets, metrics, and the statistical baseline gate |
106
+ | `tracing.py` | OTel GenAI spans: `llm_span`, `agent_span`, `record_usage`, `record_cost`, `record_attributes`, `record_hitl`, `record_exception` |
107
+ | `cost.py` | `Budget`, `CostLedger`, `estimate_cost_usd`, `estimate_tokens` |
108
+ | `types.py` | The request/response/usage/spec models everything else is typed against |
109
+
110
+ 194 names are exported. The layering is acyclic and one-directional: `errors` and `types` import nothing else in
111
+ the package, `gateway` is imported only by the package `__init__` and the eval runner, and no core module imports
112
+ a higher layer back.
113
+
114
+ ## Model pinning
115
+
116
+ An **alias resolves to an exact provider model id**, including a pinned version. That is not cosmetic: a baseline
117
+ recorded against `gpt-4o` means nothing if the alias floats to `gpt-4o-2026-…` on the next run, so an **unknown
118
+ alias is an error** (`UnknownModelAliasError`) rather than a silent fall-through to a provider default.
119
+
120
+ - `resolve(alias)` returns the `PinnedModel` (alias + `spec`); `provider_for(alias)` its provider.
121
+ - `require(alias, capabilities)` asserts the capabilities *and* returns the pinned model, checking **both** sides —
122
+ a model that supports tool calling behind an adapter that does not serialise tool definitions cannot tool-call —
123
+ and raises `CapabilityMismatchError` naming what is missing.
124
+ - `generate(alias, request, *, fallbacks=(), budget=None)` tries the alias then each fallback **in the order
125
+ given**, and reports the aliases actually attempted. Fallbacks are ordered, not conditional.
126
+ - Every charged call opens a span and updates the ledger. `CostLedger` tracks per-call `LedgerEntry` records and
127
+ yields a `SpendSummary`; `Budget` (`max_usd`, `max_total_tokens`, `scope`) is enforced *before* the call, and
128
+ `estimate_usage` / `estimate_cost_usd` price a request from a model's `CostProfile` when a caller wants to check
129
+ before spending.
130
+
131
+ ## The eval gate measures statistics, not equality
132
+
133
+ A model is **expected** to vary between runs, so a metric that compares two runs for equality is either always
134
+ red or measuring nothing. Non-determinism is not a bug to be fixed here; it is the property the gate is built to
135
+ be robust against.
136
+
137
+ - `Dataset` — versioned cases with inputs, expectations, tags and metadata; `from_records` / `from_json` /
138
+ `from_path`, `to_json`, and lookups by id (`case`) and by tag (`by_tag`).
139
+ - Metrics — `ExactMatch`, `NormalizedContains`, `JsonSchemaValid`, and `LlmJudge`. `metric_names` and
140
+ `default_metrics` cover the common set.
141
+ - `run_evaluation(dataset, runner, metrics, runs=5)` samples the runner **N times** and produces an `EvalRun` of
142
+ per-run metric means, so a single unlucky run does not decide the verdict. `evaluate_once` is the single-pass
143
+ primitive; a `DatasetRun` gives per-case outcomes and a rendered `summary()`.
144
+ - `EvalBaseline.from_run(run, model=..., note=...)` stores a baseline as JSON: per metric the sample count, mean
145
+ and spread. A committed example ships at `runtimes/python/tests/fixtures/capitals-baseline.json`.
146
+ - `BaselineGate(thresholds).evaluate(run, baseline)` compares the current sample against the baseline using a
147
+ one-sided normal test. Each `Threshold` carries a `metric`, an optional absolute `min_mean`, a
148
+ `max_regression` allowance, and an `alpha` significance level. A metric passes by clearing `min_mean`, by being
149
+ within `max_regression` of the baseline, or by regressing only within sampling noise.
150
+
151
+ Two deliberate limits:
152
+
153
+ - **`LlmJudge` is implemented and never CI-required.** A judge is itself a model call, and a gate that depends on
154
+ one non-deterministic call inside the gate you are trying to trust is the problem this design avoids.
155
+ `gateway_judge(gateway, alias)` builds the judge call when a caller wants it.
156
+ - **`GateConfigurationError` refuses a threshold on a model-derived metric**, because an N-run sample of a
157
+ probabilistic judge cannot carry a fixed threshold honestly.
158
+
159
+ `cost` is the enforced budget. A latency p95 budget belongs with dashboards, not here.
160
+
161
+ ## Retrieval
162
+
163
+ `retrieval/` is an IR-driven layer: `binding_from_spec(retriever, embedding=..., store=...)` folds the IR's
164
+ `retrievers`, `embeddings` and `stores` entries into one `RetrieverBinding`, which a `Retriever` then ingests and
165
+ searches through. The runtime consumes the names the IR emits, field for field.
166
+
167
+ - Three protocols, in the same style as `providers.Provider`: `Embedder`, `Reranker`, `VectorStore`.
168
+ - `FakeDeterministicEmbedder` maps text to a stable unit vector with a hash — keyless and offline, which is what
169
+ lets a retrieval gate run in CI. `OpenAICompatibleEmbedder` is the one hosted adapter: a single POST to
170
+ `/embeddings` over `httpx`, with the retry, timeout and error-chaining behavior of the HTTP provider.
171
+ - `LexicalReranker` is the only reranker. It is deterministic and lexical; there is no hosted rerank adapter.
172
+ - Chunk ids are **content hashes**, so re-ingesting the same input produces the same ids and a no-op diff. Ingest
173
+ is held to idempotence rather than to the byte-identical codegen guarantee.
174
+ - **Pinning.** `embed_model` names an IR `embeddings` entry and resolves at codegen time to an exact `model_id`
175
+ plus its recorded width; the resolved id, width and store `kind` travel into the generated code. An alias is
176
+ never resolved at runtime, because a baseline recorded against a floating model means nothing.
177
+ - **Cost.** Embedding calls charge the existing `CostLedger` through the existing `Budget`, using the catalog's
178
+ `CostProfile` for the pinned model. There is no retrieval-specific budgeting machinery.
179
+
180
+ `RetrievalMetric` (`RecallAtK`, `MeanReciprocalRank`, `CitationGrounding`) is deliberately a **separate protocol**
181
+ from the shipped `Metric`, which scores a single string prediction. Retrieval scores a *retrieved set*:
182
+ recall@k and MRR need the ranked ids, and citation grounding needs the passages behind the citations. Retrieval is
183
+ therefore sampled through `RetrievalGate.sample(...)`, producing a standard `EvalRun`, and then judged by the
184
+ **unchanged** `BaselineGate`. Retrieval metrics are deterministic, so they pass the gate's own eligibility check.
185
+
186
+ ### pgvector
187
+
188
+ `PgVectorStore` is a thin adapter over `psycopg` and the `vector` extension.
189
+
190
+ - It is **locally provisioned and not covered by CI.** No CI job starts a database; the in-memory adapter carries
191
+ the store tests, and both adapters share the same `VectorStore` protocol and `similarity` semantics.
192
+ `compose.pgvector.yml` in the repository brings up a database for local use.
193
+ - The `[pgvector]` extra is **import-safe when absent**: importing `aid_runtime` or `aid_runtime.retrieval` never
194
+ imports the driver, and loading the adapter raises a `ConfigurationError` naming the extra.
195
+
196
+ ## Control flow is plain async
197
+
198
+ `Pipeline` is a list of named async (or plain) steps over a `PipelineContext` that is **frozen**, so a step cannot
199
+ mutate what an earlier step observed — it returns a new context, which keeps a failed run's state inspectable. A
200
+ step may return a context, a mapping, or `None`. Each step runs in its own span, a step raising anything aborts
201
+ the run with the original exception chained (`PipelineError.__cause__`), and `StopPipeline` is the supported early
202
+ exit.
203
+
204
+ **Pydantic AI is used for structured output and nothing else** (`structured.build_agent`). The orchestration
205
+ library stays out of the control-flow path deliberately, so it remains swappable, and **no graph or orchestration
206
+ dependency is taken at all**: the agent loop in `agents/` is this package's own.
207
+
208
+ Tracing follows the OTel GenAI semantic conventions: `gen_ai.*` attributes for the operation, model, provider,
209
+ request parameters and usage, with AID-specific values namespaced `aid.*` so the two never collide. A failing call
210
+ records its exception exactly once.
211
+
212
+ ## The agent runtime
213
+
214
+ `AgentRuntime.from_ir(document, providers=..., ...)` reads the emitted IR and runs an agent to a stop condition:
215
+ model call, tool calls, observations, until a terminal answer, `max_steps`, or a budget breach. Model calls go
216
+ through `ModelGateway`, so fallback, cost accounting and tracing apply without a second implementation.
217
+
218
+ **Transfer and delegation are different, and the API says which.** `_transfer` (`handoffs`) hands the conversation
219
+ to the target and never resumes the caller, so `RunResult.agent_id` is the target and `path` grows. `_delegate`
220
+ (`delegates`) runs the target in an isolated `[system, user]` context and returns its answer as a single tool
221
+ observation, so `RunResult.agent_id` is the caller and `path` is unchanged. Both refuse to re-enter an agent
222
+ already on the path.
223
+
224
+ **A budget breach fails hard.** `BudgetExceededError` stops the run; the gateway preflight and the ledger both
225
+ enforce the ceiling. An agent declaring `on_budget_exhausted: "degrade"` (spec v0.2) instead steps exactly one rung
226
+ down its declared alias ladder — no retry loop, and the build-time gate rejects the declaration when no strictly
227
+ cheaper alias exists. Each degraded step is recorded in `StepRecord.degraded_from`, and a checkpoint carries its
228
+ current alias so a resumed run re-routes from where it stopped.
229
+
230
+ **Memory** is in-process plus one store-backed adapter. `thread` scope is the checkpointer unit: trimmed to its
231
+ `token_budget`, never written to a store. `user` and `global` scopes read and write the store and render as one
232
+ extra system message, so injection is visible in the request rather than hidden.
233
+
234
+ **Human-in-the-loop** pauses by returning a checkpoint (completed steps, pending call, current alias), and
235
+ `resume(checkpoint, decision)` continues it. The pause is recorded as an `aid.hitl` span event, not an attribute,
236
+ because a run can pause more than once.
237
+
238
+ ## Limits, stated plainly
239
+
240
+ These are refused explicitly rather than approximated — each raises `FeatureDeferredError` or a
241
+ `ConfigurationError` naming the reason:
242
+
243
+ - `ModelGateway.stream` — the transport is request/response only, and no provider advertises
244
+ `Capability.STREAMING`.
245
+ - Retrieval: non-file sources, `semantic` chunking, `incremental` refresh, retriever `filters`, a hosted
246
+ reranker, and the `qdrant` / `pinecone` / `weaviate` / `custom` store kinds.
247
+ - Escalation routing and third-party orchestration frameworks.
248
+
249
+ ## Development
250
+
251
+ From a checkout of the repository:
252
+
253
+ ```sh
254
+ pip install -e ".[dev]"
255
+ pytest
256
+ ruff check .
257
+ ruff format --check .
258
+ mypy
259
+ ```
260
+
261
+ `mypy` runs strict over `src` **and** `tests`, so the tests are type-checked too.
262
+
263
+ **Non-responsibility:** no app-specific prompts, tools, or rubrics. Those belong to the generated app's
264
+ `business/` tier — the seam this runtime is called from.
265
+
266
+ ## See also
267
+
268
+ - [`@aid-framework/runtime`](https://www.npmjs.com/package/@aid-framework/runtime) — the same runtime in
269
+ TypeScript, module for module
270
+ - [`@aid-framework/generator-py-fastapi`](https://www.npmjs.com/package/@aid-framework/generator-py-fastapi) — the
271
+ generator that emits apps against this runtime
272
+
273
+ ## License
274
+
275
+ [Apache-2.0](https://github.com/aid-framework/aid-framework/blob/main/LICENSE). Code generated by AID Framework is
276
+ not subject to this license.
@@ -0,0 +1,242 @@
1
+ # `aid-runtime-python`
2
+
3
+ The Python runtime for [AID Framework](https://github.com/aid-framework/aid-framework): the behavior layer that
4
+ apps generated by the [`py-fastapi`](https://github.com/aid-framework/aid-framework/tree/main/packages/generators/py-fastapi)
5
+ target import. Generated code is thin glue; this is where the behavior lives.
6
+
7
+ Distribution name **`aid-runtime-python`**, import package **`aid_runtime`**. The two differ deliberately: a
8
+ distribution name is conventionally hyphenated, and a Python import package cannot be. That asymmetry is asserted
9
+ by the test suite so it cannot drift.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pip install aid-runtime-python
15
+ ```
16
+
17
+ Requires Python 3.11 or newer. Dependencies are `httpx`, `pydantic`, `pydantic-ai-slim` and
18
+ `opentelemetry-api`.
19
+
20
+ Two optional extras:
21
+
22
+ ```sh
23
+ pip install "aid-runtime-python[pgvector]" # the pgvector store adapter
24
+ pip install "aid-runtime-python[deepagents]" # the deepagents agent engine, pinned
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from aid_runtime import (
31
+ Budget,
32
+ CompletionRequest,
33
+ CostProfile,
34
+ ModelGateway,
35
+ ModelSpec,
36
+ OpenAICompatibleProvider,
37
+ PinnedModel,
38
+ )
39
+
40
+ spec = ModelSpec(
41
+ name="gpt-4o-2024-08-06",
42
+ provider="openai-compatible",
43
+ cost=CostProfile(input_per_1k_usd=0.0025, output_per_1k_usd=0.01),
44
+ context_window=128_000,
45
+ max_output_tokens=4_096,
46
+ )
47
+
48
+ gateway = ModelGateway(
49
+ providers={"openai-compatible": OpenAICompatibleProvider(api_key=...)},
50
+ aliases={"small": PinnedModel(alias="small", spec=spec)},
51
+ budget=Budget(max_usd=5.0, scope="nightly"),
52
+ )
53
+
54
+ result = await gateway.generate("small", CompletionRequest.prompt("hello"))
55
+
56
+ result.text, result.usage, result.cost_usd, result.alias, result.attempts, result.fell_back
57
+ ```
58
+
59
+ ## What it provides
60
+
61
+ | Module | Provides |
62
+ | --- | --- |
63
+ | `gateway.py` | `ModelGateway` — alias resolution, capability negotiation, ordered fallback, cost accounting, GenAI spans |
64
+ | `providers/` | The `Provider` protocol, one HTTP adapter (`OpenAICompatibleProvider`), one deterministic fake (`FakeProvider`) |
65
+ | `prompts.py` | `PromptTemplate` / `PromptRegistry` — versioned prompts, typed rendering, few-shot assembly |
66
+ | `structured.py` | `build_agent` — structured output via Pydantic AI only |
67
+ | `tools.py` | `Tool` / `ToolExecutor` — declared definitions, argument validation, structured results |
68
+ | `pipeline.py` | `Pipeline` / `PipelineContext` — plain-async named steps over a frozen context |
69
+ | `agents/` | `AgentRuntime` — the loop, transfer vs. delegate, step/token/cost budgets with bounded degrade, memory, checkpointed human-in-the-loop |
70
+ | `retrieval/` | Connectors, chunkers, the `Embedder` / `Reranker` / `VectorStore` protocols, hybrid fusion, citations, retrieval metrics |
71
+ | `evals/` | Datasets, metrics, and the statistical baseline gate |
72
+ | `tracing.py` | OTel GenAI spans: `llm_span`, `agent_span`, `record_usage`, `record_cost`, `record_attributes`, `record_hitl`, `record_exception` |
73
+ | `cost.py` | `Budget`, `CostLedger`, `estimate_cost_usd`, `estimate_tokens` |
74
+ | `types.py` | The request/response/usage/spec models everything else is typed against |
75
+
76
+ 194 names are exported. The layering is acyclic and one-directional: `errors` and `types` import nothing else in
77
+ the package, `gateway` is imported only by the package `__init__` and the eval runner, and no core module imports
78
+ a higher layer back.
79
+
80
+ ## Model pinning
81
+
82
+ An **alias resolves to an exact provider model id**, including a pinned version. That is not cosmetic: a baseline
83
+ recorded against `gpt-4o` means nothing if the alias floats to `gpt-4o-2026-…` on the next run, so an **unknown
84
+ alias is an error** (`UnknownModelAliasError`) rather than a silent fall-through to a provider default.
85
+
86
+ - `resolve(alias)` returns the `PinnedModel` (alias + `spec`); `provider_for(alias)` its provider.
87
+ - `require(alias, capabilities)` asserts the capabilities *and* returns the pinned model, checking **both** sides —
88
+ a model that supports tool calling behind an adapter that does not serialise tool definitions cannot tool-call —
89
+ and raises `CapabilityMismatchError` naming what is missing.
90
+ - `generate(alias, request, *, fallbacks=(), budget=None)` tries the alias then each fallback **in the order
91
+ given**, and reports the aliases actually attempted. Fallbacks are ordered, not conditional.
92
+ - Every charged call opens a span and updates the ledger. `CostLedger` tracks per-call `LedgerEntry` records and
93
+ yields a `SpendSummary`; `Budget` (`max_usd`, `max_total_tokens`, `scope`) is enforced *before* the call, and
94
+ `estimate_usage` / `estimate_cost_usd` price a request from a model's `CostProfile` when a caller wants to check
95
+ before spending.
96
+
97
+ ## The eval gate measures statistics, not equality
98
+
99
+ A model is **expected** to vary between runs, so a metric that compares two runs for equality is either always
100
+ red or measuring nothing. Non-determinism is not a bug to be fixed here; it is the property the gate is built to
101
+ be robust against.
102
+
103
+ - `Dataset` — versioned cases with inputs, expectations, tags and metadata; `from_records` / `from_json` /
104
+ `from_path`, `to_json`, and lookups by id (`case`) and by tag (`by_tag`).
105
+ - Metrics — `ExactMatch`, `NormalizedContains`, `JsonSchemaValid`, and `LlmJudge`. `metric_names` and
106
+ `default_metrics` cover the common set.
107
+ - `run_evaluation(dataset, runner, metrics, runs=5)` samples the runner **N times** and produces an `EvalRun` of
108
+ per-run metric means, so a single unlucky run does not decide the verdict. `evaluate_once` is the single-pass
109
+ primitive; a `DatasetRun` gives per-case outcomes and a rendered `summary()`.
110
+ - `EvalBaseline.from_run(run, model=..., note=...)` stores a baseline as JSON: per metric the sample count, mean
111
+ and spread. A committed example ships at `runtimes/python/tests/fixtures/capitals-baseline.json`.
112
+ - `BaselineGate(thresholds).evaluate(run, baseline)` compares the current sample against the baseline using a
113
+ one-sided normal test. Each `Threshold` carries a `metric`, an optional absolute `min_mean`, a
114
+ `max_regression` allowance, and an `alpha` significance level. A metric passes by clearing `min_mean`, by being
115
+ within `max_regression` of the baseline, or by regressing only within sampling noise.
116
+
117
+ Two deliberate limits:
118
+
119
+ - **`LlmJudge` is implemented and never CI-required.** A judge is itself a model call, and a gate that depends on
120
+ one non-deterministic call inside the gate you are trying to trust is the problem this design avoids.
121
+ `gateway_judge(gateway, alias)` builds the judge call when a caller wants it.
122
+ - **`GateConfigurationError` refuses a threshold on a model-derived metric**, because an N-run sample of a
123
+ probabilistic judge cannot carry a fixed threshold honestly.
124
+
125
+ `cost` is the enforced budget. A latency p95 budget belongs with dashboards, not here.
126
+
127
+ ## Retrieval
128
+
129
+ `retrieval/` is an IR-driven layer: `binding_from_spec(retriever, embedding=..., store=...)` folds the IR's
130
+ `retrievers`, `embeddings` and `stores` entries into one `RetrieverBinding`, which a `Retriever` then ingests and
131
+ searches through. The runtime consumes the names the IR emits, field for field.
132
+
133
+ - Three protocols, in the same style as `providers.Provider`: `Embedder`, `Reranker`, `VectorStore`.
134
+ - `FakeDeterministicEmbedder` maps text to a stable unit vector with a hash — keyless and offline, which is what
135
+ lets a retrieval gate run in CI. `OpenAICompatibleEmbedder` is the one hosted adapter: a single POST to
136
+ `/embeddings` over `httpx`, with the retry, timeout and error-chaining behavior of the HTTP provider.
137
+ - `LexicalReranker` is the only reranker. It is deterministic and lexical; there is no hosted rerank adapter.
138
+ - Chunk ids are **content hashes**, so re-ingesting the same input produces the same ids and a no-op diff. Ingest
139
+ is held to idempotence rather than to the byte-identical codegen guarantee.
140
+ - **Pinning.** `embed_model` names an IR `embeddings` entry and resolves at codegen time to an exact `model_id`
141
+ plus its recorded width; the resolved id, width and store `kind` travel into the generated code. An alias is
142
+ never resolved at runtime, because a baseline recorded against a floating model means nothing.
143
+ - **Cost.** Embedding calls charge the existing `CostLedger` through the existing `Budget`, using the catalog's
144
+ `CostProfile` for the pinned model. There is no retrieval-specific budgeting machinery.
145
+
146
+ `RetrievalMetric` (`RecallAtK`, `MeanReciprocalRank`, `CitationGrounding`) is deliberately a **separate protocol**
147
+ from the shipped `Metric`, which scores a single string prediction. Retrieval scores a *retrieved set*:
148
+ recall@k and MRR need the ranked ids, and citation grounding needs the passages behind the citations. Retrieval is
149
+ therefore sampled through `RetrievalGate.sample(...)`, producing a standard `EvalRun`, and then judged by the
150
+ **unchanged** `BaselineGate`. Retrieval metrics are deterministic, so they pass the gate's own eligibility check.
151
+
152
+ ### pgvector
153
+
154
+ `PgVectorStore` is a thin adapter over `psycopg` and the `vector` extension.
155
+
156
+ - It is **locally provisioned and not covered by CI.** No CI job starts a database; the in-memory adapter carries
157
+ the store tests, and both adapters share the same `VectorStore` protocol and `similarity` semantics.
158
+ `compose.pgvector.yml` in the repository brings up a database for local use.
159
+ - The `[pgvector]` extra is **import-safe when absent**: importing `aid_runtime` or `aid_runtime.retrieval` never
160
+ imports the driver, and loading the adapter raises a `ConfigurationError` naming the extra.
161
+
162
+ ## Control flow is plain async
163
+
164
+ `Pipeline` is a list of named async (or plain) steps over a `PipelineContext` that is **frozen**, so a step cannot
165
+ mutate what an earlier step observed — it returns a new context, which keeps a failed run's state inspectable. A
166
+ step may return a context, a mapping, or `None`. Each step runs in its own span, a step raising anything aborts
167
+ the run with the original exception chained (`PipelineError.__cause__`), and `StopPipeline` is the supported early
168
+ exit.
169
+
170
+ **Pydantic AI is used for structured output and nothing else** (`structured.build_agent`). The orchestration
171
+ library stays out of the control-flow path deliberately, so it remains swappable, and **no graph or orchestration
172
+ dependency is taken at all**: the agent loop in `agents/` is this package's own.
173
+
174
+ Tracing follows the OTel GenAI semantic conventions: `gen_ai.*` attributes for the operation, model, provider,
175
+ request parameters and usage, with AID-specific values namespaced `aid.*` so the two never collide. A failing call
176
+ records its exception exactly once.
177
+
178
+ ## The agent runtime
179
+
180
+ `AgentRuntime.from_ir(document, providers=..., ...)` reads the emitted IR and runs an agent to a stop condition:
181
+ model call, tool calls, observations, until a terminal answer, `max_steps`, or a budget breach. Model calls go
182
+ through `ModelGateway`, so fallback, cost accounting and tracing apply without a second implementation.
183
+
184
+ **Transfer and delegation are different, and the API says which.** `_transfer` (`handoffs`) hands the conversation
185
+ to the target and never resumes the caller, so `RunResult.agent_id` is the target and `path` grows. `_delegate`
186
+ (`delegates`) runs the target in an isolated `[system, user]` context and returns its answer as a single tool
187
+ observation, so `RunResult.agent_id` is the caller and `path` is unchanged. Both refuse to re-enter an agent
188
+ already on the path.
189
+
190
+ **A budget breach fails hard.** `BudgetExceededError` stops the run; the gateway preflight and the ledger both
191
+ enforce the ceiling. An agent declaring `on_budget_exhausted: "degrade"` (spec v0.2) instead steps exactly one rung
192
+ down its declared alias ladder — no retry loop, and the build-time gate rejects the declaration when no strictly
193
+ cheaper alias exists. Each degraded step is recorded in `StepRecord.degraded_from`, and a checkpoint carries its
194
+ current alias so a resumed run re-routes from where it stopped.
195
+
196
+ **Memory** is in-process plus one store-backed adapter. `thread` scope is the checkpointer unit: trimmed to its
197
+ `token_budget`, never written to a store. `user` and `global` scopes read and write the store and render as one
198
+ extra system message, so injection is visible in the request rather than hidden.
199
+
200
+ **Human-in-the-loop** pauses by returning a checkpoint (completed steps, pending call, current alias), and
201
+ `resume(checkpoint, decision)` continues it. The pause is recorded as an `aid.hitl` span event, not an attribute,
202
+ because a run can pause more than once.
203
+
204
+ ## Limits, stated plainly
205
+
206
+ These are refused explicitly rather than approximated — each raises `FeatureDeferredError` or a
207
+ `ConfigurationError` naming the reason:
208
+
209
+ - `ModelGateway.stream` — the transport is request/response only, and no provider advertises
210
+ `Capability.STREAMING`.
211
+ - Retrieval: non-file sources, `semantic` chunking, `incremental` refresh, retriever `filters`, a hosted
212
+ reranker, and the `qdrant` / `pinecone` / `weaviate` / `custom` store kinds.
213
+ - Escalation routing and third-party orchestration frameworks.
214
+
215
+ ## Development
216
+
217
+ From a checkout of the repository:
218
+
219
+ ```sh
220
+ pip install -e ".[dev]"
221
+ pytest
222
+ ruff check .
223
+ ruff format --check .
224
+ mypy
225
+ ```
226
+
227
+ `mypy` runs strict over `src` **and** `tests`, so the tests are type-checked too.
228
+
229
+ **Non-responsibility:** no app-specific prompts, tools, or rubrics. Those belong to the generated app's
230
+ `business/` tier — the seam this runtime is called from.
231
+
232
+ ## See also
233
+
234
+ - [`@aid-framework/runtime`](https://www.npmjs.com/package/@aid-framework/runtime) — the same runtime in
235
+ TypeScript, module for module
236
+ - [`@aid-framework/generator-py-fastapi`](https://www.npmjs.com/package/@aid-framework/generator-py-fastapi) — the
237
+ generator that emits apps against this runtime
238
+
239
+ ## License
240
+
241
+ [Apache-2.0](https://github.com/aid-framework/aid-framework/blob/main/LICENSE). Code generated by AID Framework is
242
+ not subject to this license.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aid-runtime-python"
7
- version = "0.1.39"
7
+ version = "0.1.51"
8
8
  description = "The AID Framework Python runtime: model gateway with pinning and cost budgets, prompt registry, tool executor, OTel GenAI tracing, plain-async pipeline, and an eval harness that gates on statistical regression against a stored baseline."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -268,7 +268,7 @@ from aid_runtime.types import (
268
268
  )
269
269
 
270
270
  #: Kept in sync with ``pyproject.toml`` by a test, so the two cannot drift.
271
- __version__ = "0.1.39"
271
+ __version__ = "0.1.51"
272
272
 
273
273
  __all__ = [
274
274
  "ATTRIBUTES",
@@ -65,7 +65,7 @@ class RetrievalGate:
65
65
  # from these, which is the part of the protocol a retrieval metric shares.
66
66
  self._gate = BaselineGate(
67
67
  thresholds,
68
- metrics=cast("Sequence[Metric]", self._metrics),
68
+ metrics=cast(Sequence[Metric], self._metrics),
69
69
  )
70
70
 
71
71
  @property