soev_server
Serve soev agents over HTTP, with an API key, a thread index for each subject, and document collections.
The server is a FastAPI app that serves the threads API of the public profile of soev-agents. Each request sends a
bearer API key and names its subject. The server keeps the threads of each subject apart, and it lets each subject
upload documents to its own collections. Run it from the environment with the soev-server command, or build it
in Python from your own agents and stores.
Run the server
create makes the server from the SOEV_SERVER_* environment variables that Settings reads.
create
functioncreate() -> FastAPIMake the server app from the SOEV_SERVER_* environment variables.
The soev-server command runs this function with uvicorn. It reads Settings, then builds the app with
build from these parts:
- The agents of the plugins of
Settings.plugin_allowlist, from the entry point groupsoev.agents. Each agent gets, for each request, the inference client, the Postgres pool, the model ids, the default model, the context windows, the embedding model, the indexing schema and the subject of the request. - A Postgres trace store and a
PostgresThreadIndex, both inSettings.postgres_schema. - The model list and the task completions from the chat endpoints of
Settings.inference_config_root. Collectionsthat embed uploads withSettings.embed_modeland store them inSettings.indexing_schema.
At startup, the app opens the connection pool, creates the schema, the thread index table and the collection tables if they do not exist, and opens the trace store. At shutdown, it closes them and the inference client.
Example
Run the server as the soev-server command does:
import uvicorn
uvicorn.run("soev_server:create", factory=True, host="127.0.0.1", port=8000)Returns
FastAPIThe FastAPI app.
Raises:
pydantic.ValidationError: A required environment variable is missing, or a value is not valid.ValueError: The default model is not a chat endpoint of the inference configuration, or a chat endpoint has nocontext_windowfact.soev_agents.plugins.PluginError: A plugin of the allowlist does not load or is not valid.
Settings
modelSettingsThe configuration of a server that create builds and the soev-server command runs.
Each field comes from the environment variable SOEV_SERVER_ plus the field name in upper case, for example
SOEV_SERVER_API_KEY. A list field takes a JSON list, for example SOEV_SERVER_PLUGIN_ALLOWLIST='["tutorial"]'.
Settings() raises pydantic.ValidationError when a required variable is missing or a value is not valid.
attributeapi_keySecretStrThe bearer API key that each request must send in Authorization: Bearer <key>. Required, and
not empty.
attributepostgres_uriSecretStrThe URI of the Postgres database of the trace store, the thread index and the document collections. Required, and not empty.
attributeinference_config_rootPathThe directory of the soev-inference configuration. Each chat endpoint in it is a
model of the model list, and each needs a context_window fact. Required.
attributeplugin_allowlistlist[str]The names of the agent plugins to load, in order, from the entry point group
soev.agents. Required, with at least one name. Each plugin must accept the resources API
soev.server/1.
attributedefault_modelstrThe id of the chat endpoint that a fresh thread input records when it names no model.
Task completions always use it. It must be a chat endpoint of inference_config_root. Required.
attributepostgres_schemastr= 'soev_server'The Postgres schema of the trace store and of the thread index table subject_threads.
The server creates it at startup if it does not exist. The default is soev_server.
attributeembed_modelstr= 'local--embedding'The id of the embedding endpoint that embeds uploaded documents. The default is
local--embedding.
attributeindexing_schemastr= 'soev_indexing'The Postgres schema of the document collections and their chunks. The default is
soev_indexing.
attributemax_upload_bytesint= 20 * 1024 * 1024The largest document upload, in bytes. Larger uploads get 413. The default is 20 MiB.
attributehoststr= '127.0.0.1'The address that the soev-server command listens on. The default is 127.0.0.1.
attributeportint= 8000The port that the soev-server command listens on, from 1 to 65535. The default is 8000.
attributeshutdown_gracefloat= 30How many seconds the server waits for running turns at shutdown before it cancels them. The default is 30.
Build a server
build makes the server from your agents, your trace store and your thread index.
build
functionbuild(*, api_key, store, index, agents, models, default_model, model_provider, task_provider, tools_provider, resources=no_resources, collections=None, version='test', lifespan=None, shutdown_grace=30) -> FastAPIMake the server app from your agents, a trace store, a thread index and optional collections.
The app serves the threads API of the public profile of soev-agents: the threads under /v1/chat/threads, the
tool list at /v1/chat/tools, the model list at /v1/models, task completions at /v1/completions/task, the
capabilities at /v1/capabilities and the health route at /healthz. It also serves the routes of
Collections.
Each HTTP request, also to the health route and the OpenAPI docs, must send Authorization: Bearer <api_key>.
The header X-Soev-Subject names the subject of the request: a plain string, or a JWT whose sub claim the
server reads without a check of its signature. So each holder of the key can act as each subject. Without the
header, the subject is key-holder. A request without a valid key or subject gets 401.
The thread index records which subject owns each thread. A call on a thread of another subject gets 404, as
for a thread that does not exist. The thread list at GET /v1/chat/threads comes from ThreadIndex.listing.
Error responses have the media type application/problem+json, and a JSON body has a code and a detail.
A failed admission has the code unauthorized. In the threads API, the code follows the status: not_found
(404), conflict (409), invalid_request (422), service_unavailable (503) and refused (each other
status). Each error of the collection routes has the code refused. A thread event stream that fails ends
with an error event whose code is service_unavailable.
Example
from contextlib import asynccontextmanager
from fastapi import HTTPException
from soev_agents.adapters import InMemoryTraceStore
from soev_agents.entrypoints.http import ModelList, RequestContext, TaskRequest, ToolList
from soev_server import MemoryThreadIndex, build
async def list_models(ctx: RequestContext) -> ModelList:
return ModelList(data=[])
async def list_tools(ctx: RequestContext) -> ToolList:
return ToolList(data=[])
@asynccontextmanager
async def complete_task(ctx: RequestContext, body: TaskRequest):
raise HTTPException(501, "This server completes no tasks")
yield
app = build(
api_key="change-me",
store=InMemoryTraceStore(),
index=MemoryThreadIndex(),
agents={"assistant": agent}, # your soev_agents.runtime.Agent
models=["my-model"],
default_model="my-model",
model_provider=list_models,
task_provider=complete_task,
tools_provider=list_tools,
version="1.0.0",
)paramapi_keystrThe bearer API key that each request must send.
paramstoreTraceStoreThe store of the threads and their events.
paramindexThreadIndexThe thread index that records the owner of each thread and gives the thread list.
paramagentsMapping[str, Agent[Any, Any]]The agents, by the name that a client opens a thread under. The first agent runs a thread whose open call names no agent.
parammodelsSequence[str]The model ids that a thread input can name. An empty sequence accepts each model.
paramdefault_modelstrThe model that a fresh thread input records when it names no model.
parammodel_providerModelsForThe function that gives the model list at /v1/models.
paramtask_providerTaskForThe function that runs a task completion at /v1/completions/task.
paramtools_providerToolsForThe function that gives the tool list at /v1/chat/tools.
paramresourcesResourcesFor= no_resourcesThe function that gives each agent its resources for one HTTP request. The default gives
None.
paramcollectionsCollections | None= NoneThe document collections of the subjects. With None, the collection routes respond with
503.
paramversionstr= 'test'The version of the code that runs the agents. Each event that the threads append records it.
paramlifespanLifespan[FastAPI] | None= NoneThe server's own lifespan, for example to open and close the connections of store.
paramshutdown_gracefloat= 30How many seconds the server waits for running turns at shutdown before it cancels them.
Returns
FastAPIThe FastAPI app. Run it with an ASGI server, for example uvicorn.
Raises:
ValueError:api_keyis empty or only white space.
Thread indexes
A thread index records which subject owns each thread, and gives the thread list.
ThreadIndex
protocolThreadIndexA store of the threads of each subject, which the server reads to list threads and check access.
The trace store keeps the events of a thread, but not who owns it. The server records each thread in the index
when a subject opens or forks it, and removes it when the subject deletes it. Before a call on an existing
thread, the server looks the thread up for the subject of the request: if the index does not have it, the call
gets a 404 response. build takes an index, and create uses a PostgresThreadIndex.
An implementation can raise fastapi.HTTPException. The server sends its status code to the client.
methodgetasync (thread_id, subject) -> ThreadSummary | NoneReturn the summary of a thread if subject owns it.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
Returns
ThreadSummary | NoneThe summary of the thread, or None if the index has no thread with this id for this subject.
methodaddasync (row, subject) -> NoneRecord a new thread for a subject.
A thread id is unique over all subjects. For an id that the index already has, MemoryThreadIndex raises
fastapi.HTTPException with status 409, and PostgresThreadIndex raises the UniqueViolation of psycopg.
paramrowThreadSummaryThe summary of the thread. Its thread_id is the key.
paramsubjectstrThe subject that owns the thread.
methodremoveasync (thread_id, subject) -> NoneRemove a thread of a subject from the index.
The call does nothing if subject does not own a thread with this id.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
methodlistingasync (ctx, query) -> ThreadListReturn a page of the threads of the subject of a request, newest first.
The server serves this list on the thread list route. Threads sort by creation time, then by id, both descending.
paramctxRequestContextThe context of the request. Its identity names the subject.
paramqueryListQueryThe filter and the page: agent keeps only threads of that agent, before starts the page
after that thread, and limit is the page size.
Returns
ThreadListAt most query.limit thread summaries.
Raises:
fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 ifquery.beforenames a thread that the subject does not own. Both implementations of this package do this.
MemoryThreadIndex
classMemoryThreadIndex()A thread index that keeps its rows in a dictionary of the process.
The rows are lost when the process stops, and each process has its own rows. Use it for tests and for a small
server with one process. It implements ThreadIndex.
Make an empty index.
attributerowsdict[str, tuple[str, ThreadSummary]]The threads, by thread id. Each value is the owning subject and the thread summary.
methodgetasync (thread_id, subject) -> ThreadSummary | NoneReturn the summary of a thread if subject owns it.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
Returns
ThreadSummary | NoneThe summary of the thread, or None if the index has no thread with this id for this subject.
methodaddasync (row, subject) -> NoneRecord a new thread for a subject.
paramrowThreadSummaryThe summary of the thread. Its thread_id is the key.
paramsubjectstrThe subject that owns the thread.
Raises:
fastapi.HTTPException: With status 409 if the index already has a thread with this id, for any subject.
methodremoveasync (thread_id, subject) -> NoneRemove a thread of a subject from the index.
The call does nothing if subject does not own a thread with this id.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
methodlistingasync (ctx, query) -> ThreadListReturn a page of the threads of the subject of a request, newest first.
Threads sort by creation time, then by id, both descending.
paramctxRequestContextThe context of the request. Its identity names the subject.
paramqueryListQueryThe filter and the page: agent keeps only threads of that agent, before starts the page
after that thread, and limit is the page size.
Returns
ThreadListAt most query.limit thread summaries.
Raises:
fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 ifquery.beforenames a thread that the subject does not own.
PostgresThreadIndex
classPostgresThreadIndex(pool, *, schema)A thread index that keeps its rows in the table subject_threads of a Postgres schema.
It implements ThreadIndex, and create uses it. Several server processes can share one table. The index
does not create the schema: call setup after the schema exists, before the first request.
Make an index on the table subject_threads of schema.
parampoolAsyncConnectionPoolThe connection pool. Open it before the first call.
paramschemastrThe Postgres schema of the table. It must exist before setup.
attributetablesql.IdentifierThe qualified name of the table, <schema>.subject_threads.
methodsetupasync () -> NoneCreate the table and its listing index if they do not exist.
The call is idempotent, so each server process can run it at startup.
methodgetasync (thread_id, subject) -> ThreadSummary | NoneReturn the summary of a thread if subject owns it.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
Returns
ThreadSummary | NoneThe summary of the thread, or None if the table has no thread with this id for this subject.
methodaddasync (row, subject) -> NoneRecord a new thread for a subject.
paramrowThreadSummaryThe summary of the thread. Its thread_id is the primary key of the table.
paramsubjectstrThe subject that owns the thread.
Raises:
psycopg.errors.UniqueViolation: The table already has a thread with this id, for any subject.
methodremoveasync (thread_id, subject) -> NoneRemove a thread of a subject from the table.
The call does nothing if subject does not own a thread with this id.
paramthread_idstrThe id of the thread.
paramsubjectstrThe subject of the request.
methodlistingasync (ctx, query) -> ThreadListReturn a page of the threads of the subject of a request, newest first.
Threads sort by creation time, then by id, both descending.
paramctxRequestContextThe context of the request. Its identity names the subject.
paramqueryListQueryThe filter and the page: agent keeps only threads of that agent, before starts the page
after that thread, and limit is the page size.
Returns
ThreadListAt most query.limit thread summaries.
Raises:
fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 ifquery.beforenames a thread that the subject does not own.
Collections
Collections let each subject upload documents, which the server indexes for retrieval.
Collections
dataclassCollections(indexer, catalog, max_upload_bytes=20 * 1024 * 1024)The document collections of a server: the indexer that adds documents and the catalog that lists them.
Pass it to build as collections. The server then serves two routes for the subject of each request:
GET /v1/collectionslists the subject's collections fromcatalog.POST /v1/collections/{name}/documentstakes one multipart file, indexes it into collectionnameof the subject withindexer, and returns201withdocument_id,collectionandchunks. A file overmax_upload_bytesgets413, a format that the indexer does not support gets415, and a document that the indexer refuses gets422.
Without collections, both routes return 503. They also return 503 when the database, the connection pool,
the file system or the embedding model fails.
paramindexerIndexerThe indexer that parses, chunks, embeds and stores each uploaded document.
paramcatalogCatalogThe catalog that lists the collections of a subject.
parammax_upload_bytesint= 20 * 1024 * 1024The largest upload, in bytes. The default is 20 MiB.
Catalog
protocolCatalogA source of the document collections of each subject, which GET /v1/collections lists.
A soev_indexing.PgSink is a catalog.
methodlistasync (subject) -> list[Collection]Return the collections of a subject.
paramsubjectstrThe subject of the request.
Returns
list[Collection]One row for each collection of the subject. The route sends each row as a JSON object.