aid-runtime-python 0.1.39__py3-none-any.whl

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 (52) hide show
  1. aid_runtime/__init__.py +500 -0
  2. aid_runtime/agents/__init__.py +94 -0
  3. aid_runtime/agents/chat_model.py +351 -0
  4. aid_runtime/agents/deep_agents.py +600 -0
  5. aid_runtime/agents/engine.py +102 -0
  6. aid_runtime/agents/guardrails.py +519 -0
  7. aid_runtime/agents/ir.py +622 -0
  8. aid_runtime/agents/loop.py +1308 -0
  9. aid_runtime/agents/memory.py +241 -0
  10. aid_runtime/agents/policy.py +163 -0
  11. aid_runtime/agents/state.py +182 -0
  12. aid_runtime/cache.py +185 -0
  13. aid_runtime/concurrency.py +70 -0
  14. aid_runtime/control.py +329 -0
  15. aid_runtime/cost.py +168 -0
  16. aid_runtime/errors.py +337 -0
  17. aid_runtime/evals/__init__.py +72 -0
  18. aid_runtime/evals/dataset.py +77 -0
  19. aid_runtime/evals/gate.py +430 -0
  20. aid_runtime/evals/metrics.py +335 -0
  21. aid_runtime/evals/predicate.py +211 -0
  22. aid_runtime/evals/runner.py +224 -0
  23. aid_runtime/gateway.py +473 -0
  24. aid_runtime/pipeline.py +168 -0
  25. aid_runtime/prompts.py +185 -0
  26. aid_runtime/providers/__init__.py +20 -0
  27. aid_runtime/providers/base.py +57 -0
  28. aid_runtime/providers/fake.py +256 -0
  29. aid_runtime/providers/http.py +344 -0
  30. aid_runtime/py.typed +0 -0
  31. aid_runtime/retrieval/__init__.py +178 -0
  32. aid_runtime/retrieval/chunking.py +222 -0
  33. aid_runtime/retrieval/citations.py +131 -0
  34. aid_runtime/retrieval/connectors.py +131 -0
  35. aid_runtime/retrieval/embedding.py +293 -0
  36. aid_runtime/retrieval/gate.py +120 -0
  37. aid_runtime/retrieval/hybrid.py +164 -0
  38. aid_runtime/retrieval/metrics.py +213 -0
  39. aid_runtime/retrieval/pgvector.py +258 -0
  40. aid_runtime/retrieval/rerank.py +133 -0
  41. aid_runtime/retrieval/retriever.py +335 -0
  42. aid_runtime/retrieval/stores.py +161 -0
  43. aid_runtime/retrieval/types.py +433 -0
  44. aid_runtime/steps.py +284 -0
  45. aid_runtime/structured.py +150 -0
  46. aid_runtime/tools.py +271 -0
  47. aid_runtime/tracing.py +241 -0
  48. aid_runtime/types.py +332 -0
  49. aid_runtime_python-0.1.39.dist-info/METADATA +268 -0
  50. aid_runtime_python-0.1.39.dist-info/RECORD +52 -0
  51. aid_runtime_python-0.1.39.dist-info/WHEEL +5 -0
  52. aid_runtime_python-0.1.39.dist-info/top_level.txt +1 -0
@@ -0,0 +1,222 @@
1
+ """Turning documents into deterministic, size-bounded chunks.
2
+
3
+ The IR declares a strategy, a size and an overlap; it does not declare the split
4
+ algorithm. This module pins one, and the pin is the spec validator's own reading
5
+ of the intent: *"spec v0.1 chunks by size (fixed, recursive, code)"*. ``semantic``
6
+ is refused because it needs an embedding pass over the whole source at ingest --
7
+ a different shape of job, not a different boundary rule.
8
+
9
+ So all three implemented strategies are size-bounded splitters and the only thing
10
+ that distinguishes them is **where a boundary is allowed to fall**:
11
+
12
+ ``fixed``
13
+ Anywhere. The window always fills to ``size``, cutting mid-word when that is
14
+ where the budget ends. This is the honest baseline other strategies improve on.
15
+ ``recursive``
16
+ Prefer the last paragraph break, then the last line break, then the last
17
+ sentence end, then the last word break inside the window.
18
+ ``code``
19
+ Prefer the last blank line, then the last line break, then the last statement
20
+ end. Code is line-structured, so a blank line is the closest thing it has to a
21
+ paragraph, and cutting mid-statement is the failure worth avoiding.
22
+
23
+ Chunks are pure functions of the text, so the same input always produces the same
24
+ chunks with the same ids in the same order -- which is what makes re-ingesting a
25
+ corpus a no-op instead of a duplicate.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import abc
31
+ from collections.abc import Sequence
32
+ from typing import Protocol, runtime_checkable
33
+
34
+ from aid_runtime.errors import ConfigurationError, FeatureDeferredError
35
+ from aid_runtime.retrieval.connectors import SourceDocument
36
+ from aid_runtime.retrieval.types import Chunk, ChunkingSpec, ChunkingStrategy, chunk_id_for
37
+
38
+ __all__ = [
39
+ "Chunker",
40
+ "CodeChunker",
41
+ "FixedChunker",
42
+ "RecursiveChunker",
43
+ "chunk_document",
44
+ "chunker_for",
45
+ ]
46
+
47
+ #: A boundary found in the first half of a window is ignored and the window is
48
+ #: filled to ``size`` instead. Without this, one early paragraph break near the end
49
+ #: of a split would produce a stream of very short chunks, and a chunker that emits
50
+ #: 5-character chunks is worse than one that cuts a sentence in half.
51
+ _MIN_FILL_NUMERATOR = 1
52
+ _MIN_FILL_DENOMINATOR = 2
53
+
54
+
55
+ @runtime_checkable
56
+ class Chunker(Protocol):
57
+ """Splits a document into chunks, deterministically."""
58
+
59
+ @property
60
+ def strategy(self) -> ChunkingStrategy: ...
61
+
62
+ def chunk(self, document: SourceDocument) -> tuple[Chunk, ...]: ...
63
+
64
+
65
+ class _SizeChunker(abc.ABC):
66
+ """Shared windowing. Subclasses only choose where a boundary may fall."""
67
+
68
+ def __init__(self, *, size: int, overlap: int, keep_metadata: Sequence[str] = ()) -> None:
69
+ if size <= 0:
70
+ raise ConfigurationError("chunking size must be positive")
71
+ if overlap < 0:
72
+ raise ConfigurationError("chunking overlap must not be negative")
73
+ if overlap >= size:
74
+ raise ConfigurationError(
75
+ f"chunking overlap {overlap} must be smaller than size {size}; otherwise a "
76
+ "window would not advance and chunking would never terminate"
77
+ )
78
+ self._size = size
79
+ self._overlap = overlap
80
+ self._keep = tuple(keep_metadata)
81
+
82
+ @property
83
+ @abc.abstractmethod
84
+ def strategy(self) -> ChunkingStrategy: ...
85
+
86
+ @abc.abstractmethod
87
+ def _cut(self, text: str, start: int, limit: int) -> int:
88
+ """Where to end the window that starts at ``start`` and may reach ``limit``."""
89
+
90
+ def chunk(self, document: SourceDocument) -> tuple[Chunk, ...]:
91
+ text = document.text
92
+ chunks: list[Chunk] = []
93
+ start = 0
94
+ while start < len(text):
95
+ limit = min(start + self._size, len(text))
96
+ end = limit if limit >= len(text) else self._cut(text, start, limit)
97
+ # A boundary at or before `start` would stall the loop, so progress is
98
+ # guaranteed here rather than trusted from the strategy hook.
99
+ end = max(end, start + 1)
100
+ window = text[start:end]
101
+ if window.strip():
102
+ chunks.append(self._build(document, len(chunks), start, end, window))
103
+ if end >= len(text):
104
+ break
105
+ start = max(end - self._overlap, start + 1)
106
+ return tuple(chunks)
107
+
108
+ def _build(
109
+ self, document: SourceDocument, index: int, start: int, end: int, text: str
110
+ ) -> Chunk:
111
+ return Chunk(
112
+ chunk_id=chunk_id_for(document.doc_id, index, text),
113
+ doc_id=document.doc_id,
114
+ uri=document.uri,
115
+ index=index,
116
+ start=start,
117
+ end=end,
118
+ text=text,
119
+ metadata={
120
+ key: document.metadata[key] for key in self._keep if key in document.metadata
121
+ },
122
+ )
123
+
124
+ def _boundary(self, text: str, start: int, limit: int, markers: Sequence[str]) -> int | None:
125
+ """The end offset of the last ``marker`` in ``text[start:limit]``, if usable.
126
+
127
+ ``None`` means "no boundary worth taking", and the caller falls back to
128
+ ``limit``. The minimum-fill rule is applied here rather than by each
129
+ strategy so no strategy can forget it.
130
+ """
131
+ floor = start + (self._size * _MIN_FILL_NUMERATOR) // _MIN_FILL_DENOMINATOR
132
+ window = text[start:limit]
133
+ found: int | None = None
134
+ for marker in markers:
135
+ position = window.rfind(marker)
136
+ if position != -1:
137
+ end = start + position + len(marker)
138
+ if end >= max(floor, start + 1):
139
+ found = end
140
+ break
141
+ return found
142
+
143
+ def _fill(
144
+ self, text: str, start: int, limit: int, marker_groups: Sequence[Sequence[str]]
145
+ ) -> int:
146
+ """The first usable boundary from ``marker_groups``, in preference order."""
147
+ for markers in marker_groups:
148
+ found = self._boundary(text, start, limit, markers)
149
+ if found is not None:
150
+ return found
151
+ return limit
152
+
153
+
154
+ class FixedChunker(_SizeChunker):
155
+ """Cuts on size alone."""
156
+
157
+ @property
158
+ def strategy(self) -> ChunkingStrategy:
159
+ return ChunkingStrategy.FIXED
160
+
161
+ def _cut(self, text: str, start: int, limit: int) -> int:
162
+ return limit
163
+
164
+
165
+ class RecursiveChunker(_SizeChunker):
166
+ """Prefers prose boundaries: paragraph, then line, then sentence, then word."""
167
+
168
+ @property
169
+ def strategy(self) -> ChunkingStrategy:
170
+ return ChunkingStrategy.RECURSIVE
171
+
172
+ def _cut(self, text: str, start: int, limit: int) -> int:
173
+ return self._fill(
174
+ text,
175
+ start,
176
+ limit,
177
+ (("\n\n",), ("\n",), (". ", "! ", "? "), (" ",)),
178
+ )
179
+
180
+
181
+ class CodeChunker(_SizeChunker):
182
+ """Prefers code boundaries: blank line, then line, then statement, then token."""
183
+
184
+ @property
185
+ def strategy(self) -> ChunkingStrategy:
186
+ return ChunkingStrategy.CODE
187
+
188
+ def _cut(self, text: str, start: int, limit: int) -> int:
189
+ return self._fill(
190
+ text,
191
+ start,
192
+ limit,
193
+ (("\n\n",), ("\n",), (";\n", ";\r\n", "; "), (" ",)),
194
+ )
195
+
196
+
197
+ _STRATEGIES: dict[ChunkingStrategy, type[_SizeChunker]] = {
198
+ ChunkingStrategy.FIXED: FixedChunker,
199
+ ChunkingStrategy.RECURSIVE: RecursiveChunker,
200
+ ChunkingStrategy.CODE: CodeChunker,
201
+ }
202
+
203
+
204
+ def chunker_for(spec: ChunkingSpec) -> Chunker:
205
+ """Resolve a chunking block to an implementation.
206
+
207
+ ``semantic`` is refused here as well as in the spec validator, because the
208
+ runtime is reachable from generated code the validator never saw.
209
+ """
210
+ implementation = _STRATEGIES.get(spec.strategy)
211
+ if implementation is None:
212
+ raise FeatureDeferredError(
213
+ f"chunking strategy {spec.strategy.value!r} is declared but not implemented: spec "
214
+ "v0.1 chunks by size (fixed, recursive, code), and semantic chunking needs an "
215
+ "embedding pass over the whole source at ingest"
216
+ )
217
+ return implementation(size=spec.size, overlap=spec.overlap, keep_metadata=spec.keep_metadata)
218
+
219
+
220
+ def chunk_document(document: SourceDocument, spec: ChunkingSpec) -> tuple[Chunk, ...]:
221
+ """Chunk one document with the strategy the spec names."""
222
+ return chunker_for(spec).chunk(document)
@@ -0,0 +1,131 @@
1
+ """Retrieved passages, their provenance, and the context they assemble into.
2
+
3
+ A retrieved set is only useful to a model if the passages arrive with provenance
4
+ the model can repeat back, so ranking and citation are one type here rather than
5
+ a search result plus a separate lookup table. :class:`RetrievalResult` is the
6
+ runtime's answer type: it is what a ``retrieve`` step produces and what every
7
+ retrieval metric scores.
8
+
9
+ The assembled context is numbered by **rank**, not by position in the tuple. The
10
+ two are the same in practice because ranks are assigned contiguously from 1, but
11
+ naming the rank is what makes a model's ``[2]`` resolvable by the caller without
12
+ re-running the search.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Sequence
18
+
19
+ from pydantic import BaseModel, ConfigDict, Field
20
+
21
+ from aid_runtime.agents.guardrails import GuardrailRecord
22
+ from aid_runtime.retrieval.types import Chunk
23
+
24
+ __all__ = [
25
+ "Citation",
26
+ "RetrievalResult",
27
+ "RetrievedItem",
28
+ "assemble_context",
29
+ "cite",
30
+ ]
31
+
32
+ _RECORD = ConfigDict(frozen=True, extra="forbid")
33
+
34
+
35
+ class Citation(BaseModel):
36
+ """Where one retrieved passage came from.
37
+
38
+ ``doc_id`` is the document identity the IR's retrieval metric already uses
39
+ (``expectDocs: [getting-started.md]``), so a citation and a gate expectation
40
+ are the same vocabulary rather than two spellings of the same thing.
41
+ """
42
+
43
+ model_config = _RECORD
44
+
45
+ doc_id: str = Field(min_length=1)
46
+ chunk_id: str = Field(min_length=1)
47
+ uri: str = ""
48
+ index: int = Field(ge=0)
49
+
50
+ def __str__(self) -> str:
51
+ return f"{self.doc_id}#{self.index}"
52
+
53
+
54
+ class RetrievedItem(BaseModel):
55
+ """One ranked passage: the chunk, its score, and its provenance."""
56
+
57
+ model_config = _RECORD
58
+
59
+ chunk: Chunk
60
+ score: float
61
+ rank: int = Field(ge=1)
62
+ citation: Citation
63
+
64
+
65
+ class RetrievalResult(BaseModel):
66
+ """What a ``retrieve`` step returns.
67
+
68
+ ``items`` is rank-ordered and may be empty -- "the corpus has nothing for this
69
+ query" is a result, not an error, and a caller that needs a non-empty answer
70
+ says so with a metric rather than by crashing.
71
+ """
72
+
73
+ model_config = _RECORD
74
+
75
+ retriever: str = Field(min_length=1)
76
+ query: str
77
+ items: tuple[RetrievedItem, ...] = ()
78
+ #: What the ``retrieval``-stage guardrails recorded. A ``flag`` reaches a caller
79
+ #: here; a ``block`` never returns a result at all.
80
+ guardrails: tuple[GuardrailRecord, ...] = ()
81
+
82
+ def __len__(self) -> int:
83
+ return len(self.items)
84
+
85
+ def top(self, k: int) -> tuple[RetrievedItem, ...]:
86
+ """The first ``k`` items, or all of them when the result is shorter."""
87
+ return self.items[:k]
88
+
89
+ @property
90
+ def context(self) -> str:
91
+ """The numbered excerpts a prompt binds."""
92
+ return assemble_context(self.items)
93
+
94
+ @property
95
+ def citations(self) -> tuple[Citation, ...]:
96
+ """One citation per item, in rank order."""
97
+ return tuple(item.citation for item in self.items)
98
+
99
+ @property
100
+ def doc_ids(self) -> tuple[str, ...]:
101
+ """Rank-ordered, de-duplicated document ids.
102
+
103
+ De-duplicated because recall is about *documents found*, and a document
104
+ that contributed three passages was found once. First occurrence wins, so
105
+ the order still reflects relevance.
106
+ """
107
+ return tuple(dict.fromkeys(item.citation.doc_id for item in self.items))
108
+
109
+
110
+ def cite(chunk: Chunk) -> Citation:
111
+ """The citation for a chunk.
112
+
113
+ Derived from the chunk rather than passed alongside it, so a citation cannot
114
+ drift from the passage it names.
115
+ """
116
+ return Citation(
117
+ doc_id=chunk.doc_id,
118
+ chunk_id=chunk.chunk_id,
119
+ uri=chunk.uri,
120
+ index=chunk.index,
121
+ )
122
+
123
+
124
+ def assemble_context(items: Sequence[RetrievedItem], *, separator: str = "\n\n") -> str:
125
+ """Render ranked items as numbered excerpts, each tagged with its citation.
126
+
127
+ An empty retrieval renders as the empty string rather than as a header with
128
+ nothing under it: a header alone reads to a model as "sources were found", and
129
+ a prompt that invites a model to invent a source is worse than an empty prompt.
130
+ """
131
+ return separator.join(f"[{item.rank}] {item.citation} :: {item.chunk.text}" for item in items)
@@ -0,0 +1,131 @@
1
+ """Reading a ``file`` source.
2
+
3
+ The IR names five source kinds and Phase 1 implements one. That is the spec
4
+ validator's own position ("spec v0.1 indexes a local ``file`` source; the remote
5
+ loaders belong to the phase that ships them"), so the other four raise
6
+ :class:`~aid_runtime.errors.FeatureDeferredError` here rather than indexing an
7
+ empty corpus and looking healthy.
8
+
9
+ **Document identity is the POSIX path relative to the source root.** That is the
10
+ vocabulary the IR's retrieval metric already uses -- its ``expectDocs`` hold
11
+ ``getting-started.md``, not ``./docs/getting-started.md`` -- and it is
12
+ machine-independent, so the same corpus indexed on Windows and on Linux yields the
13
+ same ids and the same eval outcome. Absolute paths would make a baseline
14
+ non-portable.
15
+
16
+ The protocol is ``async`` even though the only implementation reads local files
17
+ synchronously, because the kinds that follow (``s3``, ``http``, ``db``, ``mcp``) are
18
+ genuinely concurrent. Making it sync now would be a breaking change to the seam
19
+ exactly when the first real loader arrives.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from pathlib import Path
25
+ from typing import Protocol, runtime_checkable
26
+
27
+ from pydantic import BaseModel, ConfigDict, Field
28
+
29
+ from aid_runtime.errors import ConfigurationError, FeatureDeferredError
30
+ from aid_runtime.retrieval.types import SourceKind, SourceSpec
31
+
32
+ __all__ = [
33
+ "Connector",
34
+ "FileConnector",
35
+ "SourceDocument",
36
+ "open_connector",
37
+ ]
38
+
39
+ _RECORD = ConfigDict(frozen=True, extra="forbid")
40
+
41
+
42
+ class SourceDocument(BaseModel):
43
+ """One document read from a source, before chunking."""
44
+
45
+ model_config = _RECORD
46
+
47
+ #: POSIX path relative to the source root; the document's identity everywhere.
48
+ doc_id: str = Field(min_length=1)
49
+ #: The absolute resolved location, for a human reading a citation. Never matched on.
50
+ uri: str = Field(min_length=1)
51
+ text: str
52
+ metadata: dict[str, str] = Field(default_factory=dict)
53
+
54
+
55
+ @runtime_checkable
56
+ class Connector(Protocol):
57
+ """A source that yields documents in a deterministic order."""
58
+
59
+ @property
60
+ def kind(self) -> SourceKind: ...
61
+
62
+ async def documents(self) -> tuple[SourceDocument, ...]: ...
63
+
64
+ async def aclose(self) -> None: ...
65
+
66
+
67
+ class FileConnector:
68
+ """Reads ``*.md``-style files under a local directory.
69
+
70
+ Files are read and returned in sorted ``doc_id`` order, never in directory
71
+ order: directory order is a filesystem accident, and a corpus whose chunk ids
72
+ depend on it would reindex itself differently on a different machine.
73
+ """
74
+
75
+ def __init__(self, root: str | Path, *, glob: str | None = None) -> None:
76
+ self._root = Path(root)
77
+ if not self._root.exists():
78
+ raise ConfigurationError(f"source path {str(self._root)!r} does not exist")
79
+ if not self._root.is_dir():
80
+ raise ConfigurationError(f"source path {str(self._root)!r} is not a directory")
81
+ self._glob = glob
82
+
83
+ @property
84
+ def kind(self) -> SourceKind:
85
+ return SourceKind.FILE
86
+
87
+ async def documents(self) -> tuple[SourceDocument, ...]:
88
+ return tuple(self._read(path) for path in self._paths())
89
+
90
+ async def aclose(self) -> None:
91
+ return None
92
+
93
+ def _paths(self) -> tuple[Path, ...]:
94
+ pattern = self._glob or "**/*"
95
+ found = [
96
+ path
97
+ for path in self._root.glob(pattern)
98
+ if path.is_file() and not path.name.startswith(".")
99
+ ]
100
+ return tuple(sorted(found, key=self._doc_id))
101
+
102
+ def _doc_id(self, path: Path) -> str:
103
+ return path.relative_to(self._root).as_posix()
104
+
105
+ def _read(self, path: Path) -> SourceDocument:
106
+ doc_id = self._doc_id(path)
107
+ return SourceDocument(
108
+ doc_id=doc_id,
109
+ uri=str(path.resolve()),
110
+ text=path.read_text(encoding="utf-8"),
111
+ metadata={"path": doc_id, "suffix": path.suffix},
112
+ )
113
+
114
+
115
+ def open_connector(spec: SourceSpec, *, base_dir: str | Path | None = None) -> Connector:
116
+ """Open the connector for a source entry.
117
+
118
+ ``base_dir`` resolves a relative source ``uri``; it is explicit because the
119
+ runtime has no opinion about a working directory, and a corpus must not depend
120
+ on where a process happened to be started.
121
+ """
122
+ if spec.kind is not SourceKind.FILE:
123
+ raise FeatureDeferredError(
124
+ f"source kind {spec.kind.value!r} is declared but not implemented: spec v0.1 "
125
+ "indexes a local 'file' source, and the remote loaders (s3, http, db, mcp) "
126
+ "belong to the phase that ships them"
127
+ )
128
+ root = Path(spec.uri)
129
+ if not root.is_absolute() and base_dir is not None:
130
+ root = Path(base_dir) / root
131
+ return FileConnector(root, glob=spec.glob)