soev for developers
soev-agentsReference

soev_agents.adapters

Connect agents to a model service, keep their traces in a store, and turn search hits into elements.

An implements a of soev_agents.ports over one technology. OpenAIChatModel needs the openai extra and PostgresTraceStore needs the postgres extra. The package imports without them, and each of the two names imports its library only when you use it.

Models

A model is a ChatModel over the API of a model service.

OpenAIChatModel

dataclassOpenAIChatModel(client, model, settings=dict())

A ChatModel over an OpenAI-compatible chat completions API.

Each call goes to the model that it names, else to model, at the base URL of the client. The sends the messages and the of the ChatRequest, and an image part as a base64 data URL. It reads the reasoning of the model's output from a reasoning_content or reasoning field, which OpenAI-compatible services add. It never sends reasoning back, because the API has no field for the reasoning of an earlier output.

When the service is down, slow or overloaded, the raises TransientError after the retries of the client. When the service refuses a call with another HTTP code below 500, the openai.APIStatusError of the SDK propagates.

Example

from openai import AsyncOpenAI

from soev_agents.adapters import OpenAIChatModel

model = OpenAIChatModel(AsyncOpenAI(), model="gpt-4.1-mini", settings={"temperature": 0.2})
paramclientAsyncOpenAI

The client of the OpenAI SDK. Its base URL, API key and retries apply to every call.

parammodelstr

The model that a call goes to when it names no model.

paramsettingsMapping[str, JsonValue]
= dict()

The extra fields for the body of every call, such as {"temperature": 0.2}.

methodresolve(model) -> str

Return the model that a call goes to: model, else the 's model.

parammodelstr | None

The model that the call names. None or an empty string selects the 's model.

Returns

str

The name of the model, as the model of the output records it.

methodcompleteasync (request, *, model=None) -> ModelOutput

Send request to the model in one call, and return the model's output.

The output holds the content, the reasoning, the , the finish reason and the token usage of the first choice. A finish reason other than stop, length, tool_calls and content_filter becomes None.

paramrequestChatRequest

The messages and the to send.

parammodelstr | None
= None

The model to call. None calls the 's model.

Returns

ModelOutput

The model's output. Its model is the name that resolve returns.

Raises:

  • TransientError: The service failed with HTTP code 408, 409, 429 or 500 and above, the connection failed, the body did not parse, the response held no choice, or the model wrote arguments that are not a JSON object.
  • openai.APIStatusError: The service refused the call with another HTTP code below 500.
methodstream_completeasync (request, *, model=None) -> AsyncIterator[Fragment | ModelOutput]

Send request to the model, and yield the parts of the model's output as they arrive.

The asks the service for the token usage at the end of the response, and reads only the first choice.

paramrequestChatRequest

The messages and the to send.

parammodelstr | None
= None

The model to call. None calls the 's model.

Yields:

  • Fragment | ModelOutput: A ReasoningDelta for each part of the reasoning, a string for each part of the content, and at the end the complete ModelOutput, as complete returns it.

Raises:

  • TransientError: The service failed as for complete, or the response ended without a finish reason.
  • openai.APIStatusError: The service refused the call with another HTTP code below 500.

Trace stores

A store keeps the of each , and the of the that writes them.

InMemoryTraceStore

classInMemoryTraceStore(clock=lambda: datetime.now(UTC))

TraceStore in memory, for tests and single-process serving.

An append does not await between the check of expected and its last write. So on one loop, no other append can come between them.

The are lost when the process ends. The store keeps a copy of each , made through JSON, so a later change to a object changes nothing stored.

Make an empty store.

paramclockCallable[[], datetime]
= lambda: datetime.now(UTC)

Returns the current time. The store stamps the ts of each with it, and dates the .

methodappendasync (thread_id, events, *, expected=None, lease=None) -> Sequence[StoredEvent]

Seal events into the of the , and return them as stored.

The store appends all of the or none. A refused first append leaves no . Every of the gets the same ts.

paramthread_idstr

The to append to. An unknown id starts a new .

parameventsSequence[StoredEvent]

The to append, not yet .

paramexpectedMapping[Stream, int] | None
= None

The length of each as the caller last read it. A that is not listed is not checked. None checks no .

paramleaseLease | None
= None

The that the caller holds on the . None appends without a check of the .

Returns

Sequence[StoredEvent]

The , in the order given.

Raises:

  • LeaseExpiredError: A later acquisition replaced lease, or the store holds no of the .
  • StreamMovedError: A of expected holds another number of now.
  • SealedEventError: An has a seq or a ts already.
methodreadasync (thread_id, *, after=0, follow=False) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the after the after, in the order of their .

paramthread_idstr

The to read. An unknown yields nothing, also with follow.

paramafterint
= 0

The last that the reader saw. 0 reads from the first .

paramfollowbool
= False

False yields the entries that exist now, and stops. True then waits, and yields each new entry as an append adds it. A read with follow ends when the is deleted or loaded again.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its .
methodread_backwardsasync (thread_id) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the from the newest to the oldest.

paramthread_idstr

The to read. An unknown yields nothing.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its , in the reverse order of read.
methoddeleteasync (thread_id) -> None

Remove the , its and its . A read with follow of the ends.

paramthread_idstr

The to remove. An unknown is no error.

methodacquireasync (thread_id, *, owner, ttl) -> Lease

Take the of the for ttl seconds, and return it.

The is free when no owner holds it, when the held expired, or when owner holds it already. Each acquisition adds one to the counter of the , so the new replaces the before it.

paramthread_idstr

The to take the of.

paramownerstr

The name of the process that takes the .

paramttlfloat

The seconds until the expires, unless the owner renews it.

Returns

Lease

The new .

Raises:

  • LeaseHeldError: Another owner holds a that has not expired.
methodrenewasync (lease, *, ttl) -> None

Move the expiry of lease to ttl seconds from now.

paramleaseLease

The to renew.

paramttlfloat

The seconds from now until the expires.

Raises:

  • LeaseExpiredError: The has no , or a later acquisition replaced lease.
methodreleaseasync (lease) -> None

Give up lease, so that the next acquisition does not wait for its expiry.

paramleaseLease

The to give up. A that a later acquisition replaced stays as it is.

methodleaseasync (thread_id) -> Lease | None

Return the of the while it has not expired, else None.

paramthread_idstr

The whose to read.

Returns

Lease | None

The current , or None when no owner holds one.

methodloadasync (thread_id, events) -> None

Replace the log of the with events, which are already: a restore.

The keep their seq and ts, and take the 1, 2, 3, ... in the order given. A read with follow of the replaced ends.

paramthread_idstr

The to replace. An unknown id makes a new .

parameventsSequence[StoredEvent]

The , in the order of the log.

Raises:

methodthreadsasync (*, limit, before=None) -> Sequence[tuple[str, StoredEvent]]

Return one page of the , newest first, each with its first .

Newest means the ts of the first , then the id, both descending. A without is not listed.

paramlimitint

The maximum number of on the page.

parambeforestr | None
= None

The last id of the previous page. None returns the first page. An id that the store does not hold returns an empty page.

Returns

Sequence[tuple[str, StoredEvent]]

One pair of a id and its first for each on the page.

SqliteTraceStore

classSqliteTraceStore(path, *, wake_interval=1.0, clock=lambda: datetime.now(UTC))

A TraceStore in one SQLite file, which needs no separate database process.

The file holds every . Each statement runs in a worker of asyncio.to_thread, one at a time. A write holds the write lock of the database for its whole check and insert, so another process that writes the same file cannot interleave. A read with follow wakes on each append of this store, and every wake_interval seconds for the appends of another process.

Nothing works before open, and close releases the file. async with does both. A method that you call before open raises RuntimeError.

Make a store on the file at path. The store does not touch the file before open.

parampathstr | Path

The SQLite file. open creates the file and its tables when they do not exist.

paramwake_intervalfloat
= 1.0

The seconds after which read(follow=True) reads again without an append of this store, so that the read sees what another process appended.

paramclockCallable[[], datetime]
= lambda: datetime.now(UTC)

Returns the current time. The store stamps the ts of each with it, and dates the .

methodopenasync () -> None

Open the file, and create the file and its tables when they do not exist.

A table that an earlier release of soev-agents created gets the columns that it lacks.

Raises:

  • RuntimeError: The table of lacks a column that cannot be null, so the stored rows have no value for it.
methodcloseasync () -> None

Close the file. A store that is not open stays as it is.

methodappendasync (thread_id, events, *, expected=None, lease=None) -> Sequence[StoredEvent]

Seal events into the of the , and return them as stored.

The store appends all of the or none, in one transaction. Every of the gets the same ts.

paramthread_idstr

The to append to. An unknown id starts a new .

parameventsSequence[StoredEvent]

The to append, not yet .

paramexpectedMapping[Stream, int] | None
= None

The length of each as the caller last read it. A that is not listed is not checked. None checks no .

paramleaseLease | None
= None

The that the caller holds on the . None appends without a check of the .

Returns

Sequence[StoredEvent]

The , in the order given.

Raises:

  • LeaseExpiredError: A later acquisition replaced lease, or the store holds no of the .
  • StreamMovedError: A of expected holds another number of now.
  • SealedEventError: An has a seq or a ts already.
methodloadasync (thread_id, events) -> None

Replace the log of the with events, which are already: a restore.

The keep their seq and ts, and take the 1, 2, 3, ... in the order given. A read with follow of the replaced ends.

paramthread_idstr

The to replace. An unknown id makes a new .

parameventsSequence[StoredEvent]

The , in the order of the log.

Raises:

methodreadasync (thread_id, *, after=0, follow=False) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the after the after, in the order of their .

paramthread_idstr

The to read. An unknown yields nothing, also with follow.

paramafterint
= 0

The last that the reader saw. 0 reads from the first .

paramfollowbool
= False

False yields the entries that exist now, and stops. True then waits, and yields each new entry after an append of this store, or after wake_interval seconds. A read with follow ends when the is deleted or loaded again.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its .
methodread_backwardsasync (thread_id) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the from the newest to the oldest.

The store reads all entries of the in one query, before it yields the first.

paramthread_idstr

The to read. An unknown yields nothing.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its , in the reverse order of read.
methodthreadsasync (*, limit, before=None) -> Sequence[tuple[str, StoredEvent]]

Return one page of the , newest first, each with its first .

Newest means the ts of the first , then the id, both descending. A without is not listed.

paramlimitint

The maximum number of on the page.

parambeforestr | None
= None

The last id of the previous page. None returns the first page. An id that the store does not hold returns an empty page.

Returns

Sequence[tuple[str, StoredEvent]]

One pair of a id and its first for each on the page.

methoddeleteasync (thread_id) -> None

Remove the , its and its . A read with follow of the ends.

paramthread_idstr

The to remove. An unknown is no error.

methodacquireasync (thread_id, *, owner, ttl) -> Lease

Take the of the for ttl seconds, and return it.

The is free when no owner holds it, when the held expired, or when owner holds it already. Each acquisition adds one to the counter of the , so the new replaces the before it.

paramthread_idstr

The to take the of.

paramownerstr

The name of the process that takes the .

paramttlfloat

The seconds until the expires, unless the owner renews it.

Returns

Lease

The new .

Raises:

  • LeaseHeldError: Another owner holds a that has not expired.
methodrenewasync (lease, *, ttl) -> None

Move the expiry of lease to ttl seconds from now.

paramleaseLease

The to renew.

paramttlfloat

The seconds from now until the expires.

Raises:

  • LeaseExpiredError: The has no , or a later acquisition replaced lease.
methodreleaseasync (lease) -> None

Give up lease, so that the next acquisition does not wait for its expiry.

paramleaseLease

The to give up. A that a later acquisition replaced stays as it is.

methodleaseasync (thread_id) -> Lease | None

Return the of the while it has not expired, else None.

paramthread_idstr

The whose to read.

Returns

Lease | None

The current , or None when no owner holds one.

PostgresTraceStore

classPostgresTraceStore(connection_string, *, schema, pool_size=8, wake_interval=5.0)

A TraceStore on Postgres, in a schema of its own, for many processes that serve the same .

An append holds the row lock of the until it commits. So the of one are given and become visible in order, and no other append can come between the check of expected and the insert. Each append sends a notification, and a read with follow wakes on it. The read also wakes every wake_interval seconds, because a pooler in transaction mode never delivers notifications.

Nothing works before open, which opens the connection pool and one more connection that listens for the notifications. close releases both. async with does the same, and also calls setup on entry. The schema must exist: the store creates only its tables.

The times of the come from the clock of the database. The ts of a comes from the clock of the process.

Make a store on the database at connection_string. Nothing connects before open.

paramconnection_stringstr

Where the database is, as postgresql://user:password@host:port/database or as the key=value pairs of libpq. The store gives it to psycopg unchanged.

paramschemastr

The Postgres schema that holds the tables of the store. Two stores on one database with different schemas hold different .

parampool_sizeint
= 8

The maximum number of connections in the pool, without the connection that listens.

paramwake_intervalfloat
= 5.0

The seconds after which read(follow=True) reads again without a notification, for a database behind a pooler that drops notifications.

methodopenasync () -> None

Open the connection pool and the connection that listens for notifications.

open creates no tables. Call setup for them, or use async with.

methodcloseasync () -> None

Stop the connection that listens, and close the connection pool.

methodsetupasync () -> None

Create the tables of the schema. Call it at every process start: tables that exist stay as they are.

A table that an earlier release of soev-agents created gets the columns that it lacks.

Raises:

  • RuntimeError: The table of lacks a column that cannot be null, so the stored rows have no value for it.
methodappendasync (thread_id, events, *, expected=None, lease=None) -> Sequence[StoredEvent]

Seal events into the of the , and return them as stored.

The store appends all of the or none, in one transaction. Every of the gets the same ts. Postgres cannot keep the character U+0000 in JSON, so the store removes it from each string and key of the . The returned show the as stored.

paramthread_idstr

The to append to. An unknown id starts a new .

parameventsSequence[StoredEvent]

The to append, not yet .

paramexpectedMapping[Stream, int] | None
= None

The length of each as the caller last read it. A that is not listed is not checked. None checks no .

paramleaseLease | None
= None

The that the caller holds on the . None appends without a check of the .

Returns

Sequence[StoredEvent]

The , in the order given.

Raises:

  • LeaseExpiredError: A later acquisition replaced lease, or the store holds no of the .
  • StreamMovedError: A of expected holds another number of now.
  • SealedEventError: An has a seq or a ts already.
methodloadasync (thread_id, events) -> None

Replace the log of the with events, which are already: a restore.

The keep their seq and ts, and take the 1, 2, 3, ... in the order given. The store removes U+0000 from the , as append does. A read with follow of the replaced ends.

paramthread_idstr

The to replace. An unknown id makes a new .

parameventsSequence[StoredEvent]

The , in the order of the log.

Raises:

methodreadasync (thread_id, *, after=0, follow=False) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the after the after, in the order of their .

paramthread_idstr

The to read. An unknown yields nothing, also with follow.

paramafterint
= 0

The last that the reader saw. 0 reads from the first .

paramfollowbool
= False

False yields the entries that exist now, and stops. True then waits, and yields each new entry after a notification of an append, or after wake_interval seconds. A read with follow ends when the is deleted or loaded again.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its .
methodread_backwardsasync (thread_id) -> AsyncGenerator[LogEntry[StoredEvent], None]

Yield the log entries of the from the newest to the oldest.

The store reads the entries in pages of 200, and reads the next page only when the reader asks for it.

paramthread_idstr

The to read. An unknown yields nothing.

Yields:

  • LogEntry[StoredEvent]: One log entry for each , with its , in the reverse order of read.
methodthreadsasync (*, limit, before=None) -> Sequence[tuple[str, StoredEvent]]

Return one page of the , newest first, each with its first .

Newest means the ts of the first , then the id, both descending. A without is not listed.

paramlimitint

The maximum number of on the page.

parambeforestr | None
= None

The last id of the previous page. None returns the first page. An id that the store does not hold returns an empty page.

Returns

Sequence[tuple[str, StoredEvent]]

One pair of a id and its first for each on the page.

methoddeleteasync (thread_id) -> None

Remove the , its and its . A read with follow of the ends.

paramthread_idstr

The to remove. An unknown is no error.

methodacquireasync (thread_id, *, owner, ttl) -> Lease

Take the of the for ttl seconds, and return it.

The is free when no owner holds it, when the held expired, or when owner holds it already. Each acquisition adds one to the counter of the , so the new replaces the before it.

paramthread_idstr

The to take the of.

paramownerstr

The name of the process that takes the .

paramttlfloat

The seconds until the expires, unless the owner renews it.

Returns

Lease

The new .

Raises:

  • LeaseHeldError: Another owner holds a that has not expired.
methodrenewasync (lease, *, ttl) -> None

Move the expiry of lease to ttl seconds from now.

paramleaseLease

The to renew.

paramttlfloat

The seconds from now until the expires.

Raises:

  • LeaseExpiredError: The has no , or a later acquisition replaced lease.
methodreleaseasync (lease) -> None

Give up lease, so that the next acquisition does not wait for its expiry.

paramleaseLease

The to give up. A that a later acquisition replaced stays as it is.

methodleaseasync (thread_id) -> Lease | None

Return the of the while it has not expired, else None.

paramthread_idstr

The whose to read.

Returns

Lease | None

The current , or None when no owner holds one.

Search results

A search records the hits of a soev-retrieval search as .

sources_of

functionsources_ofasync (search, elements) -> list[Element]

Await search and record what elements makes of each hit, in ranking order.

elements decides what a hit is: a passage after its document, a product that is its own whole. It sees the hit's properties as JSON values (a date becomes its ISO text). A real id is recorded once per call, as the first hit made it, so a document shared by several hits appears once, before its first passage.

A awaits its search through this, so a store problem reaches the model as the 's output instead of failing the .

paramsearchAwaitable[Sequence[Hit]]

The search of a soev-retrieval store, not yet awaited.

paramelementsCallable[[Hit], Sequence[Element]]

Makes the of one hit, in the order to record them.

Returns

list[Element]

The of all hits, in the order of the hits. Each id occurs once.

Raises:

  • TransientError: The store or a reranker raised StoreUnavailable.
  • InvalidQueryError: The store refused the call, also for a denial. The error keeps the message of the store.
  • soev_retrieval.ports.Unconfigured: The store cannot serve the call as it is built. sources_of does not convert it, so the fails.

Writing a trace store

A new TraceStore seals each of as the stores of this package do.

seal

functionseal(batch, lengths, ts) -> tuple[StoredEvent, ...]

Set the seq of each to its place in its after lengths, and set ts.

A TraceStore calls it in append, after its checks and before it writes. The of one get the next numbers in order, from the length of that stream. Every gets the same ts. The given do not change.

parambatchSequence[StoredEvent]

The of one append, not yet .

paramlengthsMapping[Stream, int]

The number of that the already holds in each . A not in it holds none.

paramtsdatetime

The time of the append.

Returns

tuple[StoredEvent, ...]

Copies of the in order, with seq and ts set.

Raises:

  • SealedEventError: An of batch has a seq or a ts already.

On this page