soev for developers
soev-server

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() -> FastAPI

Make 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 group soev.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 in Settings.postgres_schema.
  • The model list and the task completions from the chat endpoints of Settings.inference_config_root.
  • Collections that embed uploads with Settings.embed_model and store them in Settings.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

FastAPI

The 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 no context_window fact.
  • soev_agents.plugins.PluginError: A plugin of the allowlist does not load or is not valid.

Settings

modelSettings

The 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_keySecretStr

The bearer API key that each request must send in Authorization: Bearer <key>. Required, and not empty.

attributepostgres_uriSecretStr

The URI of the Postgres database of the trace store, the thread index and the document collections. Required, and not empty.

attributeinference_config_rootPath

The 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_modelstr

The 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 * 1024

The 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
= 8000

The port that the soev-server command listens on, from 1 to 65535. The default is 8000.

attributeshutdown_gracefloat
= 30

How 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) -> FastAPI

Make 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_keystr

The bearer API key that each request must send.

paramstoreTraceStore

The store of the threads and their events.

paramindexThreadIndex

The 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_modelstr

The model that a fresh thread input records when it names no model.

parammodel_providerModelsFor

The function that gives the model list at /v1/models.

paramtask_providerTaskFor

The function that runs a task completion at /v1/completions/task.

paramtools_providerToolsFor

The function that gives the tool list at /v1/chat/tools.

paramresourcesResourcesFor
= no_resources

The function that gives each agent its resources for one HTTP request. The default gives None.

paramcollectionsCollections | None
= None

The 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
= None

The server's own lifespan, for example to open and close the connections of store.

paramshutdown_gracefloat
= 30

How many seconds the server waits for running turns at shutdown before it cancels them.

Returns

FastAPI

The FastAPI app. Run it with an ASGI server, for example uvicorn.

Raises:

  • ValueError: api_key is empty or only white space.

Thread indexes

A thread index records which subject owns each thread, and gives the thread list.

ThreadIndex

protocolThreadIndex

A 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 | None

Return the summary of a thread if subject owns it.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

Returns

ThreadSummary | None

The summary of the thread, or None if the index has no thread with this id for this subject.

methodaddasync (row, subject) -> None

Record 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.

paramrowThreadSummary

The summary of the thread. Its thread_id is the key.

paramsubjectstr

The subject that owns the thread.

methodremoveasync (thread_id, subject) -> None

Remove a thread of a subject from the index.

The call does nothing if subject does not own a thread with this id.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

methodlistingasync (ctx, query) -> ThreadList

Return 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.

paramctxRequestContext

The context of the request. Its identity names the subject.

paramqueryListQuery

The 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

ThreadList

At most query.limit thread summaries.

Raises:

  • fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 if query.before names 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 | None

Return the summary of a thread if subject owns it.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

Returns

ThreadSummary | None

The summary of the thread, or None if the index has no thread with this id for this subject.

methodaddasync (row, subject) -> None

Record a new thread for a subject.

paramrowThreadSummary

The summary of the thread. Its thread_id is the key.

paramsubjectstr

The 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) -> None

Remove a thread of a subject from the index.

The call does nothing if subject does not own a thread with this id.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

methodlistingasync (ctx, query) -> ThreadList

Return a page of the threads of the subject of a request, newest first.

Threads sort by creation time, then by id, both descending.

paramctxRequestContext

The context of the request. Its identity names the subject.

paramqueryListQuery

The 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

ThreadList

At most query.limit thread summaries.

Raises:

  • fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 if query.before names 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.

parampoolAsyncConnectionPool

The connection pool. Open it before the first call.

paramschemastr

The Postgres schema of the table. It must exist before setup.

attributetablesql.Identifier

The qualified name of the table, <schema>.subject_threads.

methodsetupasync () -> None

Create 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 | None

Return the summary of a thread if subject owns it.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

Returns

ThreadSummary | None

The summary of the thread, or None if the table has no thread with this id for this subject.

methodaddasync (row, subject) -> None

Record a new thread for a subject.

paramrowThreadSummary

The summary of the thread. Its thread_id is the primary key of the table.

paramsubjectstr

The 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) -> None

Remove a thread of a subject from the table.

The call does nothing if subject does not own a thread with this id.

paramthread_idstr

The id of the thread.

paramsubjectstr

The subject of the request.

methodlistingasync (ctx, query) -> ThreadList

Return a page of the threads of the subject of a request, newest first.

Threads sort by creation time, then by id, both descending.

paramctxRequestContext

The context of the request. Its identity names the subject.

paramqueryListQuery

The 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

ThreadList

At most query.limit thread summaries.

Raises:

  • fastapi.HTTPException: With status 401 if the request has no subject, and with status 404 if query.before names 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/collections lists the subject's collections from catalog.
  • POST /v1/collections/{name}/documents takes one multipart file, indexes it into collection name of the subject with indexer, and returns 201 with document_id, collection and chunks. A file over max_upload_bytes gets 413, a format that the indexer does not support gets 415, and a document that the indexer refuses gets 422.

Without collections, both routes return 503. They also return 503 when the database, the connection pool, the file system or the embedding model fails.

paramindexerIndexer

The indexer that parses, chunks, embeds and stores each uploaded document.

paramcatalogCatalog

The catalog that lists the collections of a subject.

parammax_upload_bytesint
= 20 * 1024 * 1024

The largest upload, in bytes. The default is 20 MiB.

Catalog

protocolCatalog

A 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.

paramsubjectstr

The subject of the request.

Returns

list[Collection]

One row for each collection of the subject. The route sends each row as a JSON object.

On this page