aid-runtime-python 0.1.39__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 (94) hide show
  1. aid_runtime_python-0.1.39/PKG-INFO +268 -0
  2. aid_runtime_python-0.1.39/README.md +234 -0
  3. aid_runtime_python-0.1.39/pyproject.toml +147 -0
  4. aid_runtime_python-0.1.39/setup.cfg +4 -0
  5. aid_runtime_python-0.1.39/src/aid_runtime/__init__.py +500 -0
  6. aid_runtime_python-0.1.39/src/aid_runtime/agents/__init__.py +94 -0
  7. aid_runtime_python-0.1.39/src/aid_runtime/agents/chat_model.py +351 -0
  8. aid_runtime_python-0.1.39/src/aid_runtime/agents/deep_agents.py +600 -0
  9. aid_runtime_python-0.1.39/src/aid_runtime/agents/engine.py +102 -0
  10. aid_runtime_python-0.1.39/src/aid_runtime/agents/guardrails.py +519 -0
  11. aid_runtime_python-0.1.39/src/aid_runtime/agents/ir.py +622 -0
  12. aid_runtime_python-0.1.39/src/aid_runtime/agents/loop.py +1308 -0
  13. aid_runtime_python-0.1.39/src/aid_runtime/agents/memory.py +241 -0
  14. aid_runtime_python-0.1.39/src/aid_runtime/agents/policy.py +163 -0
  15. aid_runtime_python-0.1.39/src/aid_runtime/agents/state.py +182 -0
  16. aid_runtime_python-0.1.39/src/aid_runtime/cache.py +185 -0
  17. aid_runtime_python-0.1.39/src/aid_runtime/concurrency.py +70 -0
  18. aid_runtime_python-0.1.39/src/aid_runtime/control.py +329 -0
  19. aid_runtime_python-0.1.39/src/aid_runtime/cost.py +168 -0
  20. aid_runtime_python-0.1.39/src/aid_runtime/errors.py +337 -0
  21. aid_runtime_python-0.1.39/src/aid_runtime/evals/__init__.py +72 -0
  22. aid_runtime_python-0.1.39/src/aid_runtime/evals/dataset.py +77 -0
  23. aid_runtime_python-0.1.39/src/aid_runtime/evals/gate.py +430 -0
  24. aid_runtime_python-0.1.39/src/aid_runtime/evals/metrics.py +335 -0
  25. aid_runtime_python-0.1.39/src/aid_runtime/evals/predicate.py +211 -0
  26. aid_runtime_python-0.1.39/src/aid_runtime/evals/runner.py +224 -0
  27. aid_runtime_python-0.1.39/src/aid_runtime/gateway.py +473 -0
  28. aid_runtime_python-0.1.39/src/aid_runtime/pipeline.py +168 -0
  29. aid_runtime_python-0.1.39/src/aid_runtime/prompts.py +185 -0
  30. aid_runtime_python-0.1.39/src/aid_runtime/providers/__init__.py +20 -0
  31. aid_runtime_python-0.1.39/src/aid_runtime/providers/base.py +57 -0
  32. aid_runtime_python-0.1.39/src/aid_runtime/providers/fake.py +256 -0
  33. aid_runtime_python-0.1.39/src/aid_runtime/providers/http.py +344 -0
  34. aid_runtime_python-0.1.39/src/aid_runtime/py.typed +0 -0
  35. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/__init__.py +178 -0
  36. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/chunking.py +222 -0
  37. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/citations.py +131 -0
  38. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/connectors.py +131 -0
  39. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/embedding.py +293 -0
  40. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/gate.py +120 -0
  41. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/hybrid.py +164 -0
  42. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/metrics.py +213 -0
  43. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/pgvector.py +258 -0
  44. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/rerank.py +133 -0
  45. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/retriever.py +335 -0
  46. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/stores.py +161 -0
  47. aid_runtime_python-0.1.39/src/aid_runtime/retrieval/types.py +433 -0
  48. aid_runtime_python-0.1.39/src/aid_runtime/steps.py +284 -0
  49. aid_runtime_python-0.1.39/src/aid_runtime/structured.py +150 -0
  50. aid_runtime_python-0.1.39/src/aid_runtime/tools.py +271 -0
  51. aid_runtime_python-0.1.39/src/aid_runtime/tracing.py +241 -0
  52. aid_runtime_python-0.1.39/src/aid_runtime/types.py +332 -0
  53. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/PKG-INFO +268 -0
  54. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/SOURCES.txt +92 -0
  55. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/dependency_links.txt +1 -0
  56. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/requires.txt +17 -0
  57. aid_runtime_python-0.1.39/src/aid_runtime_python.egg-info/top_level.txt +1 -0
  58. aid_runtime_python-0.1.39/tests/test_agents_budget.py +233 -0
  59. aid_runtime_python-0.1.39/tests/test_agents_deep_agents.py +671 -0
  60. aid_runtime_python-0.1.39/tests/test_agents_hitl.py +289 -0
  61. aid_runtime_python-0.1.39/tests/test_agents_loop.py +406 -0
  62. aid_runtime_python-0.1.39/tests/test_agents_memory.py +266 -0
  63. aid_runtime_python-0.1.39/tests/test_cache.py +311 -0
  64. aid_runtime_python-0.1.39/tests/test_concurrency.py +173 -0
  65. aid_runtime_python-0.1.39/tests/test_control.py +464 -0
  66. aid_runtime_python-0.1.39/tests/test_cost.py +176 -0
  67. aid_runtime_python-0.1.39/tests/test_dataset.py +108 -0
  68. aid_runtime_python-0.1.39/tests/test_evals.py +478 -0
  69. aid_runtime_python-0.1.39/tests/test_evals_assert_gate.py +218 -0
  70. aid_runtime_python-0.1.39/tests/test_evals_gate.py +424 -0
  71. aid_runtime_python-0.1.39/tests/test_evals_predicate.py +297 -0
  72. aid_runtime_python-0.1.39/tests/test_gateway.py +337 -0
  73. aid_runtime_python-0.1.39/tests/test_guardrails.py +882 -0
  74. aid_runtime_python-0.1.39/tests/test_http_provider.py +541 -0
  75. aid_runtime_python-0.1.39/tests/test_package.py +128 -0
  76. aid_runtime_python-0.1.39/tests/test_pipeline.py +239 -0
  77. aid_runtime_python-0.1.39/tests/test_prompts.py +191 -0
  78. aid_runtime_python-0.1.39/tests/test_retrieval_chunking.py +257 -0
  79. aid_runtime_python-0.1.39/tests/test_retrieval_citations.py +151 -0
  80. aid_runtime_python-0.1.39/tests/test_retrieval_connectors.py +156 -0
  81. aid_runtime_python-0.1.39/tests/test_retrieval_embedding.py +421 -0
  82. aid_runtime_python-0.1.39/tests/test_retrieval_gate.py +515 -0
  83. aid_runtime_python-0.1.39/tests/test_retrieval_hybrid.py +169 -0
  84. aid_runtime_python-0.1.39/tests/test_retrieval_metrics.py +300 -0
  85. aid_runtime_python-0.1.39/tests/test_retrieval_rerank.py +155 -0
  86. aid_runtime_python-0.1.39/tests/test_retrieval_stores.py +336 -0
  87. aid_runtime_python-0.1.39/tests/test_retrieval_types.py +214 -0
  88. aid_runtime_python-0.1.39/tests/test_steps.py +364 -0
  89. aid_runtime_python-0.1.39/tests/test_streaming.py +198 -0
  90. aid_runtime_python-0.1.39/tests/test_structured.py +179 -0
  91. aid_runtime_python-0.1.39/tests/test_tool_permissions.py +210 -0
  92. aid_runtime_python-0.1.39/tests/test_tools.py +297 -0
  93. aid_runtime_python-0.1.39/tests/test_tracing.py +220 -0
  94. aid_runtime_python-0.1.39/tests/test_types.py +233 -0
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: aid-runtime-python
3
+ Version: 0.1.39
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 library: the behavior layer imported by apps generated by the
38
+ [`py-fastapi`](../../packages/generators/py-fastapi/) generator. Generated code is thin glue; this is where
39
+ the behavior lives ([`docs/design.md` §4](../../docs/design.md), principle 1).
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
43
+ asserted in `tests/test_package.py` so it cannot drift.
44
+
45
+ Requires Python 3.11+.
46
+
47
+ ```sh
48
+ pip install -e ".[dev]" # the runtime plus its test/lint/type tooling
49
+ pytest -q
50
+ pip install -e ".[pgvector]" # only if you want the pgvector store adapter
51
+ ```
52
+
53
+ ## What it provides
54
+
55
+ | Module | Provides |
56
+ | --- | --- |
57
+ | `gateway.py` | `ModelGateway` — alias resolution, capability negotiation, ordered fallback, cost accounting, GenAI spans |
58
+ | `providers/` | The `Provider` protocol, one HTTP adapter (`OpenAICompatibleProvider`), and one deterministic fake (`FakeProvider`) |
59
+ | `prompts.py` | `PromptTemplate` / `PromptRegistry` — versioned prompts, typed rendering, few-shot assembly |
60
+ | `structured.py` | `build_agent` — structured output via Pydantic AI only |
61
+ | `tools.py` | `Tool` / `ToolExecutor` — declared definitions, argument validation, structured results |
62
+ | `pipeline.py` | `Pipeline` / `PipelineContext` — plain-async named steps over a frozen context |
63
+ | `agents/` | The native agent runtime: `AgentRuntime` — the loop, transfer vs. delegate, step/token/cost budgets with bounded degrade, memory, and a checkpointed human-in-the-loop pause |
64
+ | `retrieval/` | Retrieval: connectors, chunkers, the `Embedder` / `Reranker` / `VectorStore` protocols, hybrid fusion, citations, and retrieval metrics |
65
+ | `evals/` | Datasets, metrics, and the statistical baseline gate |
66
+ | `tracing.py` | OTel GenAI spans: `llm_span`, `agent_span`, `record_usage`, `record_cost`, `record_attributes`, `record_hitl`, `record_exception` |
67
+ | `cost.py` | `Budget`, `CostLedger`, `estimate_cost_usd`, `estimate_tokens` |
68
+ | `types.py` | The request/response/usage/spec models everything else is typed against |
69
+
70
+ 194 names are exported from `aid_runtime`. The layering is acyclic and one-directional: `errors` and `types` import
71
+ nothing else in the package, `gateway` is imported only by the package `__init__` and the eval runner, and no
72
+ core module imports a higher layer back.
73
+
74
+ ## Model pinning
75
+
76
+ An **alias resolves to an exact provider model id**, including a pinned version. That is not cosmetic: a
77
+ baseline recorded against `gpt-4o` means nothing if the alias floats to `gpt-4o-2026-…` on the next run, so an
78
+ **unknown alias is an error** (`UnknownModelAliasError`) rather than a silent fall-through to a provider
79
+ default.
80
+
81
+ ```python
82
+ from aid_runtime import (
83
+ Budget,
84
+ CompletionRequest,
85
+ CostProfile,
86
+ ModelGateway,
87
+ ModelSpec,
88
+ OpenAICompatibleProvider,
89
+ PinnedModel,
90
+ )
91
+
92
+ spec = ModelSpec(
93
+ name="gpt-4o-2024-08-06",
94
+ provider="openai-compatible",
95
+ cost=CostProfile(input_per_1k_usd=0.0025, output_per_1k_usd=0.01),
96
+ context_window=128_000,
97
+ max_output_tokens=4096,
98
+ )
99
+
100
+ gateway = ModelGateway(
101
+ providers={"openai-compatible": OpenAICompatibleProvider(api_key=...)},
102
+ aliases={"small": PinnedModel(alias="small", spec=spec)},
103
+ budget=Budget(max_usd=5.0, scope="nightly"),
104
+ )
105
+
106
+ result = await gateway.generate("small", CompletionRequest.prompt("hello"))
107
+ result.text, result.usage, result.cost_usd, result.alias, result.attempts, result.fell_back
108
+ ```
109
+
110
+ - `resolve(alias)` returns the `PinnedModel` (alias + `spec`); `provider_for(alias)` its provider. `require(alias,
111
+ capabilities)` asserts the capabilities *and* returns the pinned model, checking **both** sides — a model that
112
+ supports tool calling behind an adapter that does not serialise tool definitions cannot tool-call — and raises
113
+ `CapabilityMismatchError` naming what is missing.
114
+ - `generate(alias, request, *, fallbacks=(), budget=None)` tries the alias then each fallback **in the order
115
+ given**, and reports the aliases actually attempted on the result. Fallbacks are ordered, not conditional.
116
+ - Every charged call opens a span and updates the ledger. `CostLedger` tracks per-call `LedgerEntry` records
117
+ and yields a `SpendSummary`; `Budget` (`max_usd`, `max_total_tokens`, `scope`) is enforced before the call,
118
+ and `estimate_usage` / `estimate_cost_usd` price a request from a model's `CostProfile` when a caller wants
119
+ to check before spending.
120
+
121
+ ## The eval harness gates on statistics, not equality
122
+
123
+ Phase 0 ratifies sampling-based gating: a model is **expected** to vary between runs, so a metric that
124
+ compares two runs for equality is either always red or measuring nothing. Non-determinism here is not a bug to
125
+ be fixed; it is the property the gate has to be robust against.
126
+
127
+ - `Dataset` — versioned cases with inputs, expectations, tags and metadata; `from_records` / `from_json` /
128
+ `from_path`, `to_json`, and lookups by id (`case`) and by tag (`by_tag`).
129
+ - Metrics — `ExactMatch`, `NormalizedContains`, `JsonSchemaValid`, and `LlmJudge`. `metric_names` and
130
+ `default_metrics` cover the common set.
131
+ - `run_evaluation(dataset, runner, metrics, runs=5)` samples the runner **N times** and produces an `EvalRun`
132
+ of per-run metric means, so a single unlucky run does not decide the verdict. `evaluate_once` is the
133
+ single-pass primitive; a `DatasetRun` gives per-case outcomes and a rendered `summary()`.
134
+ - `EvalBaseline.from_run(run, model=..., note=...)` stores a baseline as JSON: per metric the sample count,
135
+ mean and spread. `runtimes/python/tests/fixtures/capitals-baseline.json` is a committed example.
136
+ - `BaselineGate(thresholds).evaluate(run, baseline)` compares the current sample against the baseline using a
137
+ one-sided normal test, with each `Threshold` carrying `metric`, an optional absolute `min_mean`, a
138
+ `max_regression` allowance, and an `alpha` significance level. A metric passes by clearing `min_mean`, by
139
+ being within `max_regression` of the baseline, or by regressing only within sampling noise.
140
+
141
+ Two deliberate limits:
142
+
143
+ - **`LlmJudge` is implemented and never CI-required.** A judge is itself a model call, and a gate that
144
+ depends on one non-deterministic call inside the gate you are trying to trust is the problem this design
145
+ avoids. `gateway_judge(gateway, alias)` builds the judge call when a caller wants it.
146
+ - **`GateConfigurationError` refuses a threshold on a model-derived metric**, because an N-run sample of a
147
+ probabilistic judge cannot carry a fixed threshold honestly.
148
+
149
+ `cost` is the enforced budget; a latency p95 budget belongs with Phase 2 dashboards, not here.
150
+
151
+ ## Retrieval
152
+
153
+ `retrieval/` is an IR-driven layer: `binding_from_spec(retriever, embedding=..., store=...)` folds the IR's
154
+ `retrievers`, `embeddings` and `stores` entries into one `RetrieverBinding`, which a `Retriever` then ingests
155
+ and searches through. The runtime consumes the names the IR emits, field for field.
156
+
157
+ - Three protocols, in the same style as `providers.Provider`: `Embedder`, `Reranker`, `VectorStore`. Phase 1
158
+ ships the implementations listed below and no others.
159
+ - `FakeDeterministicEmbedder` maps text to a stable unit vector with a hash — keyless and offline, which is
160
+ what lets a retrieval gate run in CI. `OpenAICompatibleEmbedder` is the one hosted adapter: a single POST to
161
+ `/embeddings` over `httpx`, with the retry, timeout and error-chaining behaviour of the HTTP provider.
162
+ - `LexicalReranker` is the only reranker. It is deterministic and lexical; there is no hosted rerank adapter.
163
+ - Chunk ids are **content hashes**, so re-ingesting the same input produces the same ids and a no-op diff.
164
+ Ingest is therefore held to idempotence rather than to the byte-identical codegen guarantee.
165
+ - **Pinning.** `embedModel` names an IR `embeddings` entry and resolves at codegen time to an exact `modelId`
166
+ plus its recorded width; the resolved id, width and store `kind` travel into the generated code. An alias is
167
+ never resolved at runtime, because a baseline recorded against a floating model means nothing.
168
+ - **Cost.** Embedding calls charge the existing `CostLedger` through the existing `Budget`, using the catalog's
169
+ `CostProfile` for the pinned model. There is no retrieval-specific budgeting machinery.
170
+
171
+ `RetrievalMetric` (`RecallAtK`, `MeanReciprocalRank`, `CitationGrounding`) is deliberately a **separate
172
+ protocol** from the shipped `Metric`, which scores a single string prediction. Retrieval scores a *retrieved
173
+ set*: recall@k and MRR need the ranked ids, and citation-grounding needs the passages behind the citations.
174
+ Widening `Metric` would have changed an interface the existing gate already threshold-checks, for every
175
+ existing implementation — so retrieval is sampled through `RetrievalGate.sample(...)` instead, producing a
176
+ standard `EvalRun`, and then judged by the **unchanged** `BaselineGate`. Retrieval metrics are deterministic,
177
+ so they pass the gate's own eligibility check.
178
+
179
+ ### pgvector
180
+
181
+ `PgVectorStore` is a thin adapter over `psycopg` and the `vector` extension. Two consequences worth stating:
182
+
183
+ - It is **locally provisioned and not covered by any gate.** No CI job starts a database, and no required
184
+ check touches one; the in-memory adapter carries the store gates, and the two adapters share the same
185
+ `VectorStore` protocol and the same `similarity` semantics. `compose.pgvector.yml` beside this README brings
186
+ up a database for local use.
187
+ - The `[pgvector]` extra is **import-safe when absent**: importing `aid_runtime` or `aid_runtime.retrieval`
188
+ never imports the driver, and the adapter loading it raises a `ConfigurationError` naming the extra.
189
+
190
+ ## Control flow is plain async
191
+
192
+ `Pipeline` is a list of named async (or plain) steps over a `PipelineContext` that is **frozen**, so a step
193
+ cannot mutate what an earlier step observed — it returns a new context, which keeps a failed run's state
194
+ inspectable. A step may return a context, a mapping, or `None`. Each step runs in its own span, a step raising
195
+ anything aborts the run with the original exception chained (`PipelineError.__cause__`), and `StopPipeline` is
196
+ the supported early exit.
197
+
198
+ **Pydantic AI is used for structured output and nothing else** (`structured.build_agent`). The orchestration
199
+ library stays out of the control-flow path deliberately, so it remains swappable, and **no graph/orchestration
200
+ dependency is taken at all**: the agent loop in `agents/` is this package's own, and escalation routing and
201
+ third-party orchestration frameworks remain out of scope ([`docs/design.md` §14](../../docs/design.md)).
202
+
203
+ Tracing follows the OTel GenAI semantic conventions: `gen_ai.*` attributes for the operation, model, provider,
204
+ request parameters and usage, with AID-specific values namespaced `aid.*` so the two never collide. A failing
205
+ call records its exception exactly once.
206
+
207
+ ## The agent runtime
208
+
209
+ `AgentRuntime.from_ir(document, providers=..., ...)` reads the emitted IR and runs an agent to a stop
210
+ condition: model call, tool calls, observations, until a terminal answer, `maxSteps`, or a budget breach.
211
+ Model calls go through `ModelGateway`, so fallback, cost accounting and tracing apply without a second
212
+ implementation.
213
+
214
+ **Transfer and delegation are different, and the API says which.** `_transfer` (`handoffs`) hands the
215
+ conversation to the target and never resumes the caller, so `RunResult.agent_id` is the target and `path`
216
+ grows. `_delegate` (`delegates`) runs the target in an isolated `[system, user]` context and returns its answer
217
+ as a single tool observation, so `RunResult.agent_id` is the caller and `path` is unchanged. Both refuse to
218
+ re-enter an agent already on the path.
219
+
220
+ **A budget breach fails hard.** `BudgetExceededError` stops the run; the gateway preflight and the ledger both
221
+ enforce the ceiling. An agent declaring `onBudgetExhausted: 'degrade'` (v0.2) instead steps exactly one rung
222
+ down its declared alias ladder — no retry loop, and the build-time gate rejects the declaration when no
223
+ strictly cheaper alias exists. Each degraded step is recorded in `StepRecord.degraded_from`, and a checkpoint
224
+ carries its current alias so a resumed run re-routes from where it stopped.
225
+
226
+ **Memory** is in-process plus one store-backed adapter. `thread` scope is the checkpointer unit: trimmed to its
227
+ `tokenBudget`, never written to a store. `user` and `global` scopes read and write the store and render as one
228
+ extra system message, so injection is visible in the request rather than hidden.
229
+
230
+ **Human-in-the-loop** pauses by returning a checkpoint (completed steps, pending call, current alias), and
231
+ `resume(checkpoint, decision)` continues it. The pause is recorded as an `aid.hitl` span event, not an
232
+ attribute, because a run can pause more than once.
233
+
234
+ ## Development
235
+
236
+ Four gates, all run by the `python` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) over
237
+ real content:
238
+
239
+ ```sh
240
+ pytest
241
+ ruff check .
242
+ ruff format --check .
243
+ mypy
244
+ ```
245
+
246
+ `mypy` runs strict over `src` **and** `tests`, so the tests are type-checked too. The `python` job is a real
247
+ gate but is **advisory**: the `main` ruleset requires the `node`, `workflows` and `licenses` contexts only.
248
+
249
+ `tests/fixtures/capitals-baseline.json` is regenerated from `FakeProvider(accuracy=0.6, seed=0)` over 10 runs
250
+ — the fake is seeded and deterministic per call, so the committed baseline reproduces exactly while a fresh
251
+ sample of it still varies, which is exactly the property the gate has to tolerate.
252
+
253
+ **Non-responsibility:** no app-specific prompts, tools, or rubrics. Those belong to the generated app's
254
+ `business/` tier ([`docs/design.md` §8.4](../../docs/design.md), the seam).
255
+
256
+ **Status:** Phase 0 scope implemented, plus the Phase 1 retrieval runtime. One thing deferred and
257
+ undeclared, on purpose: `ModelGateway.stream` raises `FeatureDeferredError`, and no provider advertises
258
+ `Capability.STREAMING` or target declares the capability, because the transport is request/response-only.
259
+ Retrieval likewise fails loudly rather than
260
+ approximating: non-file sources, `semantic` chunking, `incremental` refresh, retriever `filters`, a hosted
261
+ reranker, and the `qdrant` / `pinecone` / `weaviate` / `custom` store kinds all raise `FeatureDeferredError`.
262
+ Also deferred (§14): escalation routing, memory, guardrails, prompt/semantic caching, rate limiting, token
263
+ buckets, and adapters beyond the one HTTP provider and the fake.
264
+
265
+ Emitting retrieval into a generated app and indexing it from the CLI are later layers, so this package adds no
266
+ entry point for either.
267
+
268
+ Reference: [`docs/design.md` §9, §12, §14, §17 #2](../../docs/design.md).
@@ -0,0 +1,234 @@
1
+ # AID runtime — Python
2
+
3
+ The Python runtime library: the behavior layer imported by apps generated by the
4
+ [`py-fastapi`](../../packages/generators/py-fastapi/) generator. Generated code is thin glue; this is where
5
+ the behavior lives ([`docs/design.md` §4](../../docs/design.md), principle 1).
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
9
+ asserted in `tests/test_package.py` so it cannot drift.
10
+
11
+ Requires Python 3.11+.
12
+
13
+ ```sh
14
+ pip install -e ".[dev]" # the runtime plus its test/lint/type tooling
15
+ pytest -q
16
+ pip install -e ".[pgvector]" # only if you want the pgvector store adapter
17
+ ```
18
+
19
+ ## What it provides
20
+
21
+ | Module | Provides |
22
+ | --- | --- |
23
+ | `gateway.py` | `ModelGateway` — alias resolution, capability negotiation, ordered fallback, cost accounting, GenAI spans |
24
+ | `providers/` | The `Provider` protocol, one HTTP adapter (`OpenAICompatibleProvider`), and one deterministic fake (`FakeProvider`) |
25
+ | `prompts.py` | `PromptTemplate` / `PromptRegistry` — versioned prompts, typed rendering, few-shot assembly |
26
+ | `structured.py` | `build_agent` — structured output via Pydantic AI only |
27
+ | `tools.py` | `Tool` / `ToolExecutor` — declared definitions, argument validation, structured results |
28
+ | `pipeline.py` | `Pipeline` / `PipelineContext` — plain-async named steps over a frozen context |
29
+ | `agents/` | The native agent runtime: `AgentRuntime` — the loop, transfer vs. delegate, step/token/cost budgets with bounded degrade, memory, and a checkpointed human-in-the-loop pause |
30
+ | `retrieval/` | Retrieval: connectors, chunkers, the `Embedder` / `Reranker` / `VectorStore` protocols, hybrid fusion, citations, and retrieval metrics |
31
+ | `evals/` | Datasets, metrics, and the statistical baseline gate |
32
+ | `tracing.py` | OTel GenAI spans: `llm_span`, `agent_span`, `record_usage`, `record_cost`, `record_attributes`, `record_hitl`, `record_exception` |
33
+ | `cost.py` | `Budget`, `CostLedger`, `estimate_cost_usd`, `estimate_tokens` |
34
+ | `types.py` | The request/response/usage/spec models everything else is typed against |
35
+
36
+ 194 names are exported from `aid_runtime`. The layering is acyclic and one-directional: `errors` and `types` import
37
+ nothing else in the package, `gateway` is imported only by the package `__init__` and the eval runner, and no
38
+ core module imports a higher layer back.
39
+
40
+ ## Model pinning
41
+
42
+ An **alias resolves to an exact provider model id**, including a pinned version. That is not cosmetic: a
43
+ baseline recorded against `gpt-4o` means nothing if the alias floats to `gpt-4o-2026-…` on the next run, so an
44
+ **unknown alias is an error** (`UnknownModelAliasError`) rather than a silent fall-through to a provider
45
+ default.
46
+
47
+ ```python
48
+ from aid_runtime import (
49
+ Budget,
50
+ CompletionRequest,
51
+ CostProfile,
52
+ ModelGateway,
53
+ ModelSpec,
54
+ OpenAICompatibleProvider,
55
+ PinnedModel,
56
+ )
57
+
58
+ spec = ModelSpec(
59
+ name="gpt-4o-2024-08-06",
60
+ provider="openai-compatible",
61
+ cost=CostProfile(input_per_1k_usd=0.0025, output_per_1k_usd=0.01),
62
+ context_window=128_000,
63
+ max_output_tokens=4096,
64
+ )
65
+
66
+ gateway = ModelGateway(
67
+ providers={"openai-compatible": OpenAICompatibleProvider(api_key=...)},
68
+ aliases={"small": PinnedModel(alias="small", spec=spec)},
69
+ budget=Budget(max_usd=5.0, scope="nightly"),
70
+ )
71
+
72
+ result = await gateway.generate("small", CompletionRequest.prompt("hello"))
73
+ result.text, result.usage, result.cost_usd, result.alias, result.attempts, result.fell_back
74
+ ```
75
+
76
+ - `resolve(alias)` returns the `PinnedModel` (alias + `spec`); `provider_for(alias)` its provider. `require(alias,
77
+ capabilities)` asserts the capabilities *and* returns the pinned model, checking **both** sides — a model that
78
+ supports tool calling behind an adapter that does not serialise tool definitions cannot tool-call — and raises
79
+ `CapabilityMismatchError` naming what is missing.
80
+ - `generate(alias, request, *, fallbacks=(), budget=None)` tries the alias then each fallback **in the order
81
+ given**, and reports the aliases actually attempted on the result. Fallbacks are ordered, not conditional.
82
+ - Every charged call opens a span and updates the ledger. `CostLedger` tracks per-call `LedgerEntry` records
83
+ and yields a `SpendSummary`; `Budget` (`max_usd`, `max_total_tokens`, `scope`) is enforced before the call,
84
+ and `estimate_usage` / `estimate_cost_usd` price a request from a model's `CostProfile` when a caller wants
85
+ to check before spending.
86
+
87
+ ## The eval harness gates on statistics, not equality
88
+
89
+ Phase 0 ratifies sampling-based gating: a model is **expected** to vary between runs, so a metric that
90
+ compares two runs for equality is either always red or measuring nothing. Non-determinism here is not a bug to
91
+ be fixed; it is the property the gate has to be robust against.
92
+
93
+ - `Dataset` — versioned cases with inputs, expectations, tags and metadata; `from_records` / `from_json` /
94
+ `from_path`, `to_json`, and lookups by id (`case`) and by tag (`by_tag`).
95
+ - Metrics — `ExactMatch`, `NormalizedContains`, `JsonSchemaValid`, and `LlmJudge`. `metric_names` and
96
+ `default_metrics` cover the common set.
97
+ - `run_evaluation(dataset, runner, metrics, runs=5)` samples the runner **N times** and produces an `EvalRun`
98
+ of per-run metric means, so a single unlucky run does not decide the verdict. `evaluate_once` is the
99
+ single-pass primitive; a `DatasetRun` gives per-case outcomes and a rendered `summary()`.
100
+ - `EvalBaseline.from_run(run, model=..., note=...)` stores a baseline as JSON: per metric the sample count,
101
+ mean and spread. `runtimes/python/tests/fixtures/capitals-baseline.json` is a committed example.
102
+ - `BaselineGate(thresholds).evaluate(run, baseline)` compares the current sample against the baseline using a
103
+ one-sided normal test, with each `Threshold` carrying `metric`, an optional absolute `min_mean`, a
104
+ `max_regression` allowance, and an `alpha` significance level. A metric passes by clearing `min_mean`, by
105
+ being within `max_regression` of the baseline, or by regressing only within sampling noise.
106
+
107
+ Two deliberate limits:
108
+
109
+ - **`LlmJudge` is implemented and never CI-required.** A judge is itself a model call, and a gate that
110
+ depends on one non-deterministic call inside the gate you are trying to trust is the problem this design
111
+ avoids. `gateway_judge(gateway, alias)` builds the judge call when a caller wants it.
112
+ - **`GateConfigurationError` refuses a threshold on a model-derived metric**, because an N-run sample of a
113
+ probabilistic judge cannot carry a fixed threshold honestly.
114
+
115
+ `cost` is the enforced budget; a latency p95 budget belongs with Phase 2 dashboards, not here.
116
+
117
+ ## Retrieval
118
+
119
+ `retrieval/` is an IR-driven layer: `binding_from_spec(retriever, embedding=..., store=...)` folds the IR's
120
+ `retrievers`, `embeddings` and `stores` entries into one `RetrieverBinding`, which a `Retriever` then ingests
121
+ and searches through. The runtime consumes the names the IR emits, field for field.
122
+
123
+ - Three protocols, in the same style as `providers.Provider`: `Embedder`, `Reranker`, `VectorStore`. Phase 1
124
+ ships the implementations listed below and no others.
125
+ - `FakeDeterministicEmbedder` maps text to a stable unit vector with a hash — keyless and offline, which is
126
+ what lets a retrieval gate run in CI. `OpenAICompatibleEmbedder` is the one hosted adapter: a single POST to
127
+ `/embeddings` over `httpx`, with the retry, timeout and error-chaining behaviour of the HTTP provider.
128
+ - `LexicalReranker` is the only reranker. It is deterministic and lexical; there is no hosted rerank adapter.
129
+ - Chunk ids are **content hashes**, so re-ingesting the same input produces the same ids and a no-op diff.
130
+ Ingest is therefore held to idempotence rather than to the byte-identical codegen guarantee.
131
+ - **Pinning.** `embedModel` names an IR `embeddings` entry and resolves at codegen time to an exact `modelId`
132
+ plus its recorded width; the resolved id, width and store `kind` travel into the generated code. An alias is
133
+ never resolved at runtime, because a baseline recorded against a floating model means nothing.
134
+ - **Cost.** Embedding calls charge the existing `CostLedger` through the existing `Budget`, using the catalog's
135
+ `CostProfile` for the pinned model. There is no retrieval-specific budgeting machinery.
136
+
137
+ `RetrievalMetric` (`RecallAtK`, `MeanReciprocalRank`, `CitationGrounding`) is deliberately a **separate
138
+ protocol** from the shipped `Metric`, which scores a single string prediction. Retrieval scores a *retrieved
139
+ set*: recall@k and MRR need the ranked ids, and citation-grounding needs the passages behind the citations.
140
+ Widening `Metric` would have changed an interface the existing gate already threshold-checks, for every
141
+ existing implementation — so retrieval is sampled through `RetrievalGate.sample(...)` instead, producing a
142
+ standard `EvalRun`, and then judged by the **unchanged** `BaselineGate`. Retrieval metrics are deterministic,
143
+ so they pass the gate's own eligibility check.
144
+
145
+ ### pgvector
146
+
147
+ `PgVectorStore` is a thin adapter over `psycopg` and the `vector` extension. Two consequences worth stating:
148
+
149
+ - It is **locally provisioned and not covered by any gate.** No CI job starts a database, and no required
150
+ check touches one; the in-memory adapter carries the store gates, and the two adapters share the same
151
+ `VectorStore` protocol and the same `similarity` semantics. `compose.pgvector.yml` beside this README brings
152
+ up a database for local use.
153
+ - The `[pgvector]` extra is **import-safe when absent**: importing `aid_runtime` or `aid_runtime.retrieval`
154
+ never imports the driver, and the adapter loading it raises a `ConfigurationError` naming the extra.
155
+
156
+ ## Control flow is plain async
157
+
158
+ `Pipeline` is a list of named async (or plain) steps over a `PipelineContext` that is **frozen**, so a step
159
+ cannot mutate what an earlier step observed — it returns a new context, which keeps a failed run's state
160
+ inspectable. A step may return a context, a mapping, or `None`. Each step runs in its own span, a step raising
161
+ anything aborts the run with the original exception chained (`PipelineError.__cause__`), and `StopPipeline` is
162
+ the supported early exit.
163
+
164
+ **Pydantic AI is used for structured output and nothing else** (`structured.build_agent`). The orchestration
165
+ library stays out of the control-flow path deliberately, so it remains swappable, and **no graph/orchestration
166
+ dependency is taken at all**: the agent loop in `agents/` is this package's own, and escalation routing and
167
+ third-party orchestration frameworks remain out of scope ([`docs/design.md` §14](../../docs/design.md)).
168
+
169
+ Tracing follows the OTel GenAI semantic conventions: `gen_ai.*` attributes for the operation, model, provider,
170
+ request parameters and usage, with AID-specific values namespaced `aid.*` so the two never collide. A failing
171
+ call records its exception exactly once.
172
+
173
+ ## The agent runtime
174
+
175
+ `AgentRuntime.from_ir(document, providers=..., ...)` reads the emitted IR and runs an agent to a stop
176
+ condition: model call, tool calls, observations, until a terminal answer, `maxSteps`, or a budget breach.
177
+ Model calls go through `ModelGateway`, so fallback, cost accounting and tracing apply without a second
178
+ implementation.
179
+
180
+ **Transfer and delegation are different, and the API says which.** `_transfer` (`handoffs`) hands the
181
+ conversation to the target and never resumes the caller, so `RunResult.agent_id` is the target and `path`
182
+ grows. `_delegate` (`delegates`) runs the target in an isolated `[system, user]` context and returns its answer
183
+ as a single tool observation, so `RunResult.agent_id` is the caller and `path` is unchanged. Both refuse to
184
+ re-enter an agent already on the path.
185
+
186
+ **A budget breach fails hard.** `BudgetExceededError` stops the run; the gateway preflight and the ledger both
187
+ enforce the ceiling. An agent declaring `onBudgetExhausted: 'degrade'` (v0.2) instead steps exactly one rung
188
+ down its declared alias ladder — no retry loop, and the build-time gate rejects the declaration when no
189
+ strictly cheaper alias exists. Each degraded step is recorded in `StepRecord.degraded_from`, and a checkpoint
190
+ carries its current alias so a resumed run re-routes from where it stopped.
191
+
192
+ **Memory** is in-process plus one store-backed adapter. `thread` scope is the checkpointer unit: trimmed to its
193
+ `tokenBudget`, never written to a store. `user` and `global` scopes read and write the store and render as one
194
+ extra system message, so injection is visible in the request rather than hidden.
195
+
196
+ **Human-in-the-loop** pauses by returning a checkpoint (completed steps, pending call, current alias), and
197
+ `resume(checkpoint, decision)` continues it. The pause is recorded as an `aid.hitl` span event, not an
198
+ attribute, because a run can pause more than once.
199
+
200
+ ## Development
201
+
202
+ Four gates, all run by the `python` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) over
203
+ real content:
204
+
205
+ ```sh
206
+ pytest
207
+ ruff check .
208
+ ruff format --check .
209
+ mypy
210
+ ```
211
+
212
+ `mypy` runs strict over `src` **and** `tests`, so the tests are type-checked too. The `python` job is a real
213
+ gate but is **advisory**: the `main` ruleset requires the `node`, `workflows` and `licenses` contexts only.
214
+
215
+ `tests/fixtures/capitals-baseline.json` is regenerated from `FakeProvider(accuracy=0.6, seed=0)` over 10 runs
216
+ — the fake is seeded and deterministic per call, so the committed baseline reproduces exactly while a fresh
217
+ sample of it still varies, which is exactly the property the gate has to tolerate.
218
+
219
+ **Non-responsibility:** no app-specific prompts, tools, or rubrics. Those belong to the generated app's
220
+ `business/` tier ([`docs/design.md` §8.4](../../docs/design.md), the seam).
221
+
222
+ **Status:** Phase 0 scope implemented, plus the Phase 1 retrieval runtime. One thing deferred and
223
+ undeclared, on purpose: `ModelGateway.stream` raises `FeatureDeferredError`, and no provider advertises
224
+ `Capability.STREAMING` or target declares the capability, because the transport is request/response-only.
225
+ Retrieval likewise fails loudly rather than
226
+ approximating: non-file sources, `semantic` chunking, `incremental` refresh, retriever `filters`, a hosted
227
+ reranker, and the `qdrant` / `pinecone` / `weaviate` / `custom` store kinds all raise `FeatureDeferredError`.
228
+ Also deferred (§14): escalation routing, memory, guardrails, prompt/semantic caching, rate limiting, token
229
+ buckets, and adapters beyond the one HTTP provider and the fake.
230
+
231
+ Emitting retrieval into a generated app and indexing it from the CLI are later layers, so this package adds no
232
+ entry point for either.
233
+
234
+ Reference: [`docs/design.md` §9, §12, §14, §17 #2](../../docs/design.md).
@@ -0,0 +1,147 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aid-runtime-python"
7
+ version = "0.1.39"
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
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "The AID Framework authors" }]
13
+ keywords = ["ai", "codegen", "llm", "evals", "opentelemetry", "pydantic"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Typing :: Typed",
22
+ ]
23
+ # Deliberately narrow. One HTTP provider adapter ships here; provider breadth is
24
+ # out of scope. `pydantic-ai-slim` is used only for structured output, not as an
25
+ # orchestration dependency -- pipeline control flow is plain async.
26
+ dependencies = [
27
+ "httpx>=0.27,<1",
28
+ "opentelemetry-api>=1.24,<2",
29
+ "pydantic>=2.7,<3",
30
+ "pydantic-ai-slim>=2,<3",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ # pgvector is optional and locally provisioned. The driver is imported lazily inside
35
+ # the adapter, so the default install and the `dev` extra both work with this extra
36
+ # absent -- which is the only state CI is ever in. There is deliberately no CI job
37
+ # for it; the adapter is not gate-covered, and the in-memory store carries the gates.
38
+ pgvector = ["psycopg[binary]>=3.1,<4"]
39
+ # The Deep Agents orchestration backend. Exactly pinned, never a range: the IR gate
40
+ # demands a pinned `engineVersion`, and `aid_runtime.agents.engine.DEEPAGENTS_PIN`
41
+ # is the one authoritative copy of this number that a run is compared against. Both
42
+ # copies must move together, which is why the version is a single literal here and a
43
+ # single literal there rather than a constraint that could silently widen.
44
+ deepagents = ["deepagents==0.7.32"]
45
+ dev = [
46
+ "mypy>=1.11,<3",
47
+ "opentelemetry-sdk>=1.24,<2",
48
+ "pytest>=8,<10",
49
+ "pytest-asyncio>=0.24,<2",
50
+ "ruff>=0.6,<1",
51
+ ]
52
+
53
+ [project.urls]
54
+ Homepage = "https://github.com/aid-framework/aid-framework"
55
+ Documentation = "https://github.com/aid-framework/aid-framework/blob/main/docs/design.md"
56
+ Issues = "https://github.com/aid-framework/aid-framework/issues"
57
+
58
+ [tool.setuptools.packages.find]
59
+ where = ["src"]
60
+
61
+ [tool.setuptools.package-data]
62
+ aid_runtime = ["py.typed"]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
66
+ asyncio_mode = "auto"
67
+ asyncio_default_fixture_loop_scope = "function"
68
+ addopts = "-ra --strict-markers --strict-config"
69
+ filterwarnings = ["error"]
70
+
71
+ [tool.ruff]
72
+ line-length = 100
73
+ target-version = "py311"
74
+ src = ["src", "tests"]
75
+
76
+ [tool.ruff.lint]
77
+ select = [
78
+ "A", # builtins shadowing
79
+ "ANN", # annotations
80
+ "ASYNC", # async correctness
81
+ "B", # bugbear
82
+ "C4", # comprehensions
83
+ "DTZ", # naive datetimes
84
+ "E", # pycodestyle errors
85
+ "ERA", # commented-out code
86
+ "F", # pyflakes
87
+ "I", # import sorting
88
+ "LOG", # logging
89
+ "N", # pep8-naming
90
+ "PT", # pytest style
91
+ "PTH", # pathlib over os.path
92
+ "RET", # return simplification
93
+ "RUF", # ruff-specific
94
+ "S", # bandit
95
+ "SIM", # simplify
96
+ "T20", # no print()
97
+ "TID", # tidy imports
98
+ "UP", # pyupgrade
99
+ "W", # pycodestyle warnings
100
+ ]
101
+ ignore = [
102
+ # `Any` is unavoidable at the provider/tool boundary, where the payload is
103
+ # shaped by the remote model rather than by us.
104
+ "ANN401",
105
+ ]
106
+
107
+ [tool.ruff.lint.per-file-ignores]
108
+ "tests/**" = [
109
+ "ANN001", # fixtures and test callables take untyped parameters
110
+ "ANN201",
111
+ "ANN202",
112
+ "S101", # assert is the point of a test
113
+ ]
114
+ # The fake provider is a deterministic simulation, not a source of randomness
115
+ # with security implications; bandit cannot tell the difference.
116
+ "src/aid_runtime/providers/fake.py" = ["S311"]
117
+
118
+ [tool.ruff.lint.flake8-tidy-imports]
119
+ ban-relative-imports = "all"
120
+
121
+ [tool.mypy]
122
+ python_version = "3.11"
123
+ mypy_path = "src"
124
+ files = ["src", "tests"]
125
+ strict = true
126
+ warn_unreachable = true
127
+
128
+ [[tool.mypy.overrides]]
129
+ # pydantic-ai ships type hints but no `py.typed` marker on every release line.
130
+ module = ["pydantic_ai.*"]
131
+ follow_untyped_imports = true
132
+
133
+ [[tool.mypy.overrides]]
134
+ # The pgvector driver is an optional extra and is absent wherever the gates run, so
135
+ # there is nothing to resolve. Everything read off it is confined to `Any` inside the
136
+ # one adapter that is allowed to touch it.
137
+ module = ["psycopg.*"]
138
+ ignore_missing_imports = true
139
+
140
+ [[tool.mypy.overrides]]
141
+ # The Deep Agents harness is an optional extra. `deepagents` and `langgraph` are absent
142
+ # from the default install, and `langchain`'s middleware surface is not exported with
143
+ # types we can rely on across minor releases. Everything crossing this boundary is
144
+ # erased at the one adapter in `agents/chat_model.py` and the one engine in
145
+ # `agents/deep_agents.py`; the rest of the runtime never sees these names.
146
+ module = ["deepagents.*", "langgraph.*", "langchain.*", "langchain_core.*"]
147
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+