Data model¶
AAA is meant to work over your data, whatever shape it has. This page explains exactly where your data enters the
system today, what the Item/Codec layer is for, and where the current boundaries are.
Where your data enters¶
| Path | Use it for | Types |
|---|---|---|
| Run input | The task and its parameters. | Any JSON-serializable dict ({"task": ..., "customer_id": ...}). |
| Tool arguments and results | Everything the agent reads or writes in your systems. | Anything Pydantic can validate: models, dataclasses, TypedDict, lists, dicts, primitives. The tool's JSON schema is generated from the type hints. |
| Indexed documents | Knowledge the agent retrieves. | Doc(id, text, meta) in an Index. |
| Events | Anything that happens around a run. | JSON data, optionally versioned with a schema. |
Item + Codec |
Your own algorithms and tools that need to render, persist or fingerprint arbitrary values uniformly. | Any type, via a registered codec. |
A boundary to know about
The built-in algorithms take a plain-dict input and do their work through tools and the LLM; none of them
requires an Item. Item and Codec are the uniform data layer you use in your own tools and algorithms
(and that the extension points are built around). Making the built-in algorithms
consume Items natively is on the roadmap.
Items and codecs¶
An Item[T] wraps a user value in an envelope the library can reason about:
| Field | |
|---|---|
id |
ULID by default. |
kind |
Open string selecting the codec ("ticket", "invoice-row"). |
payload |
Your value, untouched. |
meta |
Free-form dict. |
provenance |
Provenance(source, parents, ts, version): where it came from. |
content_hash |
SHA-256 of the canonical JSON, for dedup and caching. |
embedding, blobs |
Optional vector and blob pointers. |
A codec teaches AAA four things about a type: render it to text for an LLM (to_text, with truncation), turn it
into JSON and back (to_json/from_json), describe it (json_schema), and fingerprint it.
"""Arbitrary data: the same API over Pydantic models, dataclasses, dicts, text and custom types."""
import dataclasses
from pydantic import BaseModel
from aaa.data import CodecRegistry, FunctionCodec, MissingCapability, preflight
class Ticket(BaseModel):
id: int
title: str
priority: str = "normal"
@dataclasses.dataclass
class Row:
sku: str
qty: int
codecs = CodecRegistry()
codecs.register("ticket", Ticket) # a Pydantic model
codecs.register("row", Row) # a dataclass
codecs.register("event", dict) # a plain dict
codecs.register("doc", str) # plain text
codecs.register( # anything else: teach AAA how to render and serialize it
"frame",
FunctionCodec(
to_text=lambda r: ", ".join(f"{k}={v}" for k, v in r.items()),
to_json=dict,
from_json=dict,
),
)
items = [
codecs.wrap("ticket", Ticket(id=7, title="Printer on fire", priority="high")),
codecs.wrap("row", Row(sku="A-1", qty=3)),
codecs.wrap("event", {"type": "login", "ok": False}),
codecs.wrap("doc", "A long document body " * 20),
codecs.wrap("frame", {"col_a": 1, "col_b": "x"}),
]
for item in items:
# every kind renders to LLM-ready text, can be truncated, and has a stable content hash
print(f"{item.kind:7} {item.content_hash[:8]} {codecs.render(item, max_chars=48)!r}")
# round-trip through JSON and back to the user's own type
restored = codecs.get("row").from_json(codecs.get("row").to_json(Row("A-1", 3)))
assert restored == Row("A-1", 3)
# identical content => identical hash, which is what dedup and caching key on
assert codecs.wrap("event", {"type": "login", "ok": False}).content_hash == items[2].content_hash
try: # fail fast at startup if an algorithm needs something you did not provide
preflight({"scorer", "embedder"}, {"embedder": object()})
except MissingCapability as e:
print("preflight:", e)
ticket f7199c89 '{\n "id": 7,\n "priority": "high",\n "title": "…'
row d578aa73 '{\n "qty": 3,\n "sku": "A-1"\n}'
event 81fa4071 '{\n "ok": false,\n "type": "login"\n}'
doc 2d87e6fc 'A long document body A long document body A lon…'
frame 78efaf7c 'col_a=1, col_b=x'
preflight: missing capabilities: scorer
TypeAdapterCodec(used automatically when you register a type) covers Pydantic models, dataclasses, dicts, lists and primitives.FunctionCodecis the escape hatch for anything else: DataFrame rows, images, protobufs, ORM objects.- Keys are sorted before hashing, so equal content gives equal hashes regardless of key order.
Capability protocols and preflight¶
Algorithms and tools can declare what they need from your environment by name, and fail fast at startup instead of mid-run:
from aaa.data import preflight
preflight({"embedder", "scorer"}, {"embedder": my_embedder}) # raises MissingCapability: scorer
The protocols are Embedder (async embed(texts) -> vectors), Scorer (async score(item) -> float) and
Keyer (key(item) -> Hashable for dedup).
Retrieval: Index¶
from aaa.data import Doc, InMemoryIndex
index = InMemoryIndex(embedder) # embedder optional
await index.upsert([Doc(id="d1", text="...", meta={"source": "wiki"})])
hits = await index.search("capital of France", k=5, filter=lambda d: d.meta["source"] == "wiki")
InMemoryIndex does BM25 keyword search and, when you give it an Embedder, fuses it with cosine similarity using
reciprocal-rank fusion. It is brute force: fine to roughly 10⁵ documents. For more, implement the small Index
protocol (upsert, delete, search, count) on top of pgvector, Qdrant or similar; search_tool(index) and
RAGAgent accept any implementation. A production vector-store adapter is not shipped yet.
Large payloads¶
Values too big for an event live in a content-addressed blob store and the event carries a pointer. That is covered in Large payloads.
Why events carry JSON, not your types¶
Events are written to a log that outlives your code. Storing JSON (validated against a versioned schema if you
register one) means a deployment can change a Python class without invalidating history, other languages can read
the log, and a SELECT in Postgres is a debugging tool.