Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ AgentExperience.NET is a **preview**. It is not production ready, and public API
- **The Python and Go ports still pass contract 1.5**, not 1.6. Their suites accept a 1.6 lock and skip the four vector
families by name until stories 23.2 and 23.3 implement them.

### Python semantic retrieval (story 23.2)

- **The Python port passes contract 1.6.** `use_agent_experience(..., embedder=...)` takes your embedding callable (an
object with `model_id`, `dimension` and a sync or async `embed(text)`; no model is a dependency). With one, injection
runs the text and the vector channel together and merges them as contract 1.6 says, then ranks with the unchanged
ranking, so a reworded query finds a lesson it shares no words with. Without one, or when the vector channel fails,
it answers from text alone and reports why: `NotConfigured`, `ProviderUnavailable`, `ModelMismatch`,
`DimensionMismatch` or `VectorSearchFailed`, through `on_vector_fallback` and `InjectionResult.vector_fallback`. The
vector channel never fails the call and is bounded by `vector_timeout` (0.5 seconds by default); the text channel's
failure still fails it. `min_vector_relevance` drops weak vector candidates, never text ones.
- **Stores.** `InMemoryExperienceStore` and `PostgresExperienceStore` (PostgreSQL with pgvector, running the contract's
two new SQL files as written) both serve the channel through `write_embedding` and `search_vectors`. After a record is
stored, its summary's embedding is written in the background, outside the record's own commit; a failure is reported
to `on_capture_failure` as the new stage `Index` and never fails capture.
- Without an embedder nothing changes. The Python test suite now needs a pgvector image (`pgvector/pgvector`) for its
PostgreSQL leg. The Go port still passes contract 1.5.

## 0.1.0-preview.14

This is the first preview that publishes the Python and Go ports. The same tag now releases the six NuGet packages, the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ lesson was injected) is published as [a language-neutral contract](contract/READ
[what 1.5 adds](contract/README-1.5.md), [what 1.6 adds](contract/README-1.6.md): retrieval by meaning, with
embeddings): a JSON Schema, the SQL to run, and
fixtures generated from and checked against this implementation on every build. A pure-Python core that passes
contract 1.5, with a one-call Python Agent Framework integration, is in [`python/`](python/README.md). A lesson a .NET
contract 1.6 (including retrieval by meaning, with an embedder you register), with a one-call Python Agent Framework integration, is in [`python/`](python/README.md). A lesson a .NET
agent learned is injected into a Python agent, and the other way round. A Go core and PostgreSQL store that pass contract 1.5, with a one-call Go Agent
Framework integration, are in [`go/`](go/README.md). A lesson a .NET agent learned is injected into a Go agent, and
the other way round. Both are published with each release, at the same version as the NuGet packages (a Python alpha,
Expand Down
45 changes: 41 additions & 4 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
A pure-Python core of AgentExperience that implements the [language-neutral contract](https://github.com/fabbrik/AgentExperience.NET/blob/main/contract/README.md), so a
lesson learned by a Python agent is stored in the format the .NET library reads, and the other way round.

**Status: alpha, contract 1.5, with a Microsoft Agent Framework integration (the `maf` extra).** It passes every
fixture of contract 1.5. It is published to PyPI as `agenticexperience` (the import name stays `agentexperience`), with every
**Status: alpha, contract 1.6, with a Microsoft Agent Framework integration (the `maf` extra).** It passes every
fixture of contract 1.6. It is published to PyPI as `agenticexperience` (the import name stays `agentexperience`), with every
release of this repository and at the same version as the .NET packages: .NET `0.1.0-preview.N` is
`agenticexperience` `0.1.0aN` (a PEP 440 alpha; while every release is an alpha, pip and uv install the newest one).
Like the .NET packages, it claims no production readiness, and its API may change between alphas.
Expand Down Expand Up @@ -64,6 +64,9 @@ prints each run and the block. Its output is checked in as
`additional_properties["AgentExperience.HistoricalReference"] = True` (`message_role="system"` sends it as a system
message). An agent that already holds an `ExperienceContextProvider` gets no second one; `inject=False` captures
without injecting.
- **Semantic retrieval** (contract 1.6), with an embedder you register (below). Injection then runs the text and the
vector channel together, merges them and ranks as before; each stored record's summary is embedded in the
background.
- **Injection tracking** (contract 1.4). Once a block is handed over, a store with `record_injection` (the PostgreSQL
and in-memory stores) is told which records it delivered, so the .NET lessons view shows when each lesson was last
injected, by a Python agent too. It runs in a background task on the event loop, bounded by
Expand Down Expand Up @@ -96,6 +99,40 @@ prints each run and the block. Its output is checked in as
(`agentexperience.classifiers`) have the semantics of `ExperienceToolResultClassifiers`: `exit_code(*tools)` and
`http_status(*tools)` read a JSON object's top-level fields, else the last match in the text.

### Semantic retrieval: registering an embedder

Without an embedder retrieval is text only, and a reworded query finds nothing it shares no words with. Register an
embedding callable (any model: none is a dependency) and retrieval becomes hybrid:

```python
class Embedder:
model_id = "my-embedding-model" # stored with every vector; vectors of another model are never compared
dimension = 1536 # the vector's length, known without calling the provider

def embed(self, text: str) -> list[float]: # or `async def`; exactly `dimension` finite numbers
return my_provider.embed(text)

agent = use_agent_experience(agent, store=store, identity=identity, verify=verify, embedder=Embedder())
```

- **In-memory and PostgreSQL stores.** `InMemoryExperienceStore` compares 32-bit floats by cosine distance.
`PostgresExperienceStore` needs PostgreSQL with pgvector, the .NET vectors package's migrations (`0004`, `0020`) and
the grants of `sql/application-role-privileges-1.6.sql` after the 1.0 and 1.4 files (or the .NET privileges call).
- **Fail-open.** The vector channel never fails or delays past `vector_timeout` (seconds, default 0.5, covering the
embedding and the search) the text answer. When it does not contribute the injection answers from text and reports
the reason through `on_vector_fallback(VectorFallback)` and `InjectionResult.vector_fallback`: `NotConfigured` (no
embedder, or a store without `write_embedding` and `search_vectors`), `ProviderUnavailable` (the embedder raised, ran
out of time, or returned a blank model ID or an empty, wrong-length or non-finite vector; no statement runs),
`ModelMismatch` or `DimensionMismatch` (every stored vector is of another model or width) and `VectorSearchFailed`.
A failure of the text search still fails the injection, as without an embedder. `min_vector_relevance` (0 to 1) drops
vector candidates below it; text candidates are never dropped.
- **Writing embeddings.** After a record is stored, a background task embeds its summary (task ID, task summary and
lesson) and writes it, only while the record is still at that revision. It never runs inside the record's own commit
and never fails capture: a failure is reported to `on_capture_failure` with the stage `Index`.
`await settings.wait_for_index_writes()` (also on the provider) awaits the ones still running. Records stored before
an embedder was registered have no vector until one is written; the .NET library's re-index does it for a shared
database.

### In production

The identity must come from your own authentication, never from model output: pass a function of an
Expand Down Expand Up @@ -128,7 +165,7 @@ which shares lessons with .NET agents on the same database.
`sql/application-role-privileges.sql`, then `sql/application-role-privileges-1.4.sql` (without the second, every
injection is still made and its record is reported `NotRecorded`).

It does nothing outside contract 1.x: no grants, crypto-shredding, provenance signing or vectors, and no lifecycle
It does nothing outside contract 1.x: no grants, crypto-shredding or provenance signing, and no lifecycle
after finalization (reuse evidence, reinforcement, contest, revocation). It writes no logs at all, so no record
content is ever logged.

Expand Down Expand Up @@ -258,7 +295,7 @@ uv run mypy --strict src tests
uv run pytest
```

`uv run pytest` runs the unit tests and the conformance runner (`tests/test_contract.py`), which loads every fixture
`uv run pytest` (its PostgreSQL leg uses a `pgvector/pgvector` image) runs the unit tests and the conformance runner (`tests/test_contract.py`), which loads every fixture
family from [`../contract/fixtures`](https://github.com/fabbrik/AgentExperience.NET/tree/main/contract/fixtures) and compares this package's output exactly: blocks and task
text byte for byte, JSON by value. It validates every record against the contract's JSON Schema, checks every
contract file against `contract/lock.json`, and fails with a clear message if the lock names a contract version this
Expand Down
34 changes: 29 additions & 5 deletions python/src/agentexperience/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
AgentExperience for Python: a pure-Python core that implements the AgentExperience.NET language-neutral contract 1.5.
AgentExperience for Python: a pure-Python core that implements the AgentExperience.NET language-neutral contract 1.6.

Pre-release. It finalizes a captured run into a stored Experience Record, renders the injected Historical Reference
block, derives the task text, classifies errors, and (with the ``postgres`` extra) writes records to and searches them
Expand All @@ -13,6 +13,19 @@

from .block import Block, BlockOptions, RankedRecord, RankingComponent, render_block
from .capture import CapturedRun, RawAttempt, RawToolCall, RunCapture
from .embedding import (
Embedder,
EmbeddingWrite,
EmbeddingWriteResult,
FallbackReason,
VectorFallback,
VectorHit,
VectorQuery,
VectorSearchResult,
content_hash,
merge_channels,
retrieval_summary,
)
from .encoding import Duration, Instant
from .error_class import error_class
from .finalize import FinalizedExperience, InitialLifecycleEvent, finalize
Expand Down Expand Up @@ -49,11 +62,11 @@
except _metadata.PackageNotFoundError: # running from a source tree that was never installed
__version__ = "0.0.0+unknown"

CONTRACT_VERSION = "1.5"
"""The contract version this package passes: every fixture of contract 1.5, and so every 1.0, 1.1, 1.2, 1.3 and 1.4
fixture."""
CONTRACT_VERSION = "1.6"
"""The contract version this package passes: every fixture of contract 1.6, and so every 1.0, 1.1, 1.2, 1.3, 1.4 and
1.5 fixture."""

SUPPORTED_CONTRACT_VERSIONS: tuple[str, ...] = ("1.0", "1.1", "1.2", "1.3", "1.4", "1.5")
SUPPORTED_CONTRACT_VERSIONS: tuple[str, ...] = ("1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6")
"""Every contract version whose fixtures this package passes."""

__all__ = [
Expand All @@ -67,9 +80,13 @@
"CapturedRun",
"ClosedVerificationRound",
"Duration",
"Embedder",
"EmbeddingWrite",
"EmbeddingWriteResult",
"EnvironmentFingerprint",
"Evidence",
"ExperienceRecord",
"FallbackReason",
"FinalizedExperience",
"InitialLifecycleEvent",
"Instant",
Expand All @@ -95,14 +112,21 @@
"SearchRow",
"SearchSafetyError",
"ToolCall",
"VectorFallback",
"VectorHit",
"VectorQuery",
"VectorSearchResult",
"content_hash",
"derive_task_text",
"error_class",
"experience_id_for",
"finalize",
"from_stored",
"initial_event_id_for",
"merge_channels",
"rank",
"reflection_id_for",
"render_block",
"retrieval_summary",
"to_stored",
]
Loading