API¶
Core app & server¶
Shared FastAPI app factory for Klea packages.
File: klea_utils/api/app.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.api.app.make_app(graph_factory: Callable[[], BaseLangGraph], title: str = 'Klea API', version: str = '0.1.0', routers: list[APIRouter] | None = None) FastAPI[source]¶
Create a FastAPI instance with a standard lifespan.
The lifespan:
Instantiates and sets up the graph via graph_factory
Opens a persistent
SessionStoreat{graph.paths.user_data_dir}/sessions.dbalongside the graph’s checkpoints.Stores the graph and session store on
app.state
- Parameters:
graph_factory – Callable that returns a configured
BaseLangGraphinstancetitle – API title (appears in OpenAPI docs)
version – API version (appears in OpenAPI docs)
routers – List of APIRouters to include on the app
- Returns:
Configured FastAPI app
Shared server launcher factory for Klea packages.
File: klea_utils/api/server.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.api.server.configure_profile(profile: str | None, config_env_var: str | None, config_dir: str | Path | None, template_writer: Callable[[Path], Path] | None) None[source]¶
Apply a
--profilevalue for the current process.The profile is carried into the app module through the environment: config_env_var is set to
<name>.jsonand left in place (the env var takes precedence over the env file in pydantic-settings). The profile name is validated against the working directory / config_dir first so a typo fails fast.templateis special: it writes a scaffold config into the working directory and exits, never launching anything.- Parameters:
profile – Raw
--profilevalue (a trailing.jsonis stripped);Noneis a no-opconfig_env_var – Environment variable that names the config file, or
Noneto skip setting oneconfig_dir – Config directory used for validation;
Noneskips the fast-fail checktemplate_writer – Callable that writes a config template into the working directory and returns its path;
Nonedisables--profile template
- Raises:
typer.BadParameter – If the profile does not resolve to a file
typer.Exit – After writing a template
- klea_utils.api.server.is_loopback_host(host: str) bool[source]¶
Return
Truewhen host refers to the local machine.- Parameters:
host – Hostname from a server URL (e.g.
"127.0.0.1")- Returns:
Truefor loopback addresses,Falseotherwise
- klea_utils.api.server.make_serve_app(app_module: str, default_port: int = 8005, config_env_var: str | None = None, config_dir: str | Path | None = None, template_writer: Callable[[Path], Path] | None = None) Typer[source]¶
Create a Typer app that runs uvicorn on the given app_module.
The module string should be the importable path to a FastAPI
appinstance, e.g."klea_rag.api.main:app".The
servecommand accepts a--profileoption that selects the JSON config file for the app. The value is validated (seeconfigure_profile()); when config_env_var is given it is forwarded to the app module through that environment variable, so a profile dropped in the working directory or the config directory is used without any other wiring. The special profiletemplatescaffolds a new config and exits instead of launching.- Parameters:
app_module – Uvicorn module string
default_port – Default port number
config_env_var – Environment variable that carries the config file name into the app process (e.g.
"KLEA_RAG_APP_CONFIG_FILE")config_dir – Config directory searched after the working directory when validating
--profiletemplate_writer – Callable that writes a config template into the working directory for
--profile template
- Returns:
A
typer.Typerapp for use as a CLI entry point
- klea_utils.api.server.spawn_server(app_module: str, host: str = '127.0.0.1', port: int = 8005, timeout: float = 180.0, profile: str | None = None) Iterator[Popen | None][source]¶
Context manager that runs an API server in a subprocess.
If a healthy server is already listening at
host:port(probed via/health/ready), nothing is spawned andNoneis yielded, so the caller does not own the server’s lifecycle. Otherwise a uvicorn subprocess is spawned (stdout and stderr inherited so startup errors and app output are visible; access logs are disabled to keep the shared terminal clean, with full logging preserved in the server’s rotating log file), readiness is waited on, and the subprocess is terminated when thewithblock exits.When profile is given but a server is already running, the profile cannot take effect (the running server was started with its own config) and a warning is printed.
- Parameters:
app_module – Uvicorn module string (e.g.
"klea_rag.api.main:app")host – Host to bind
port – Port to bind
timeout – Total seconds to wait for readiness after spawning
profile – Config profile the caller requested (used only to warn when the requested profile cannot apply)
- Returns:
The spawned
subprocess.Popen(orNoneif an existing server was reused)
- klea_utils.api.server.split_server_url(url: str, default_port: int = 8005) tuple[str, int][source]¶
Return
(host, port)parsed from url.Falls back to
127.0.0.1and default_port when the URL does not carry a hostname or port.- Parameters:
url – Server URL (e.g.
http://127.0.0.1:8005)default_port – Port to use when the URL omits one
- Returns:
(host, port)suitable for binding a local server
Chat endpoints¶
Shared chat endpoint plumbing for Klea packages.
This module provides the generic plumbing the chat endpoints
(/query and /query/stream) all need: readiness checks, session
persistence, per-request model overrides, SSE framing, and error
handling. The API contract itself is app-specific – each app’s
api/chat.py defines its own ChatPayload model and its own
endpoint functions, wired through the helpers here, so the router can
expose whichever fields the app contract needs (e.g. the agent’s
mode) without growing this shared module.
The enrich hook lets an app inject app-level events into the SSE
stream (for example a context event carrying the agent operating
mode), while the shared framing stays here.
File: klea_utils/api/chat_core.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- async klea_utils.api.chat_core.run_query(request: Request, *, query: str, user_id: str, chat_id: str, extra_state: dict[str, Any] | None = None, context_fields: dict[str, Any] | None = None) str[source]¶
Run the graph via
run_graph_invokeand persist the exchange.Applies the stored per-chat model overrides for the duration of the call (via the LangGraph Runtime context, ADR-0033), maps graph errors onto HTTP status codes, and writes the user query + assistant answer to the session store.
- Parameters:
request – Request carrying
app.state.graph/chat_sessionsquery – User query text
user_id – Persistent user identifier
chat_id – Chat conversation identifier
extra_state – Optional app-specific initial state fields passed to the graph invocation (e.g. the agent’s operating
moderequest).context_fields – Optional app-defined per-run context fields that the plumbing forwards together with the framework-provided
model_overridesslice (ADR-0033). The assembled dict is coerced/validated against the app’s registeredcontext_schemaat the graph boundary;KleaRunContextisextra="allow"(or the app subclasses it for typed fields). Apps wire frontend payload fields through this generic hook instead of forkingchat_core.
- Returns:
The assistant’s answer text
- Note:
POST /queryreturns only the answer string; the session context (operating mode etc., ADR-0032) is not included. It is not produced on the bareainvokepath (run_graph_invoke) – fetch it via/query/streamcontextevents or the hydration endpointGET /chat/{user_id}/{chat_id}/context.- Raises:
HTTPException – 400 on
ValueError, 503 onRuntimeError, 500 on any other failure
- klea_utils.api.chat_core.stream_response(request: Request, *, query: str, user_id: str, chat_id: str, enrich: Callable[[AsyncIterator[dict]], AsyncIterator[dict]] | None = None, extra_state: dict[str, Any] | None = None, context_fields: dict[str, Any] | None = None) StreamingResponse[source]¶
Return a
/query/streamSSE response for the graph’s events.Applies the stored per-chat model overrides for the duration of the stream, persists the user query + final assistant answer on the
completeevent, and converts graph failures intoerrorSSE events instead of dropping the stream.- Parameters:
request – Request carrying
app.state.graph/chat_sessionsquery – User query text
user_id – Persistent user identifier
chat_id – Chat conversation identifier
enrich – Optional async-generator wrapper applied to the raw
run_graph_astream_eventsevent stream before framing. Apps use it to inject app-specific events (e.g. acontextevent with the operating mode) or filter events. WhenNone, every graph event is emitted unchanged.extra_state – Optional app-specific initial state fields passed to the graph invocation (e.g. the agent’s operating
moderequest).context_fields – Optional app-defined per-run context fields that the plumbing forwards together with the framework-provided
model_overridesslice (ADR-0033). The assembled dict is coerced/validated against the app’s registeredcontext_schemaat the graph boundary;KleaRunContextisextra="allow"(or the app subclasses it for typed fields). Apps wire frontend payload fields through this generic hook instead of forkingchat_core.
- Returns:
A
fastapi.responses.StreamingResponseSSE stream
- klea_utils.api.chat_core.thread_id_for(user_id: str, chat_id: str) str[source]¶
Return the checkpoint thread id for a
{user_id}:{chat_id}pair.
Shared health check endpoint factory for Klea packages.
File: klea_utils/api/health.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.api.health.create_health_router() APIRouter[source]¶
Create an APIRouter with
/health/liveand/health/readyendpoints.
Message history endpoints for chat sessions.
File: klea_utils/api/messages.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
Session management¶
Chat session CRUD endpoints.
NOTE: user_id is currently a browser-generated UUID taken from
the URL path. For multi-user deployments this must be replaced with
an authenticated identity (JWT / OAuth) extracted from the request
context — otherwise any user can delete or rename another user’s chats
by modifying the user_id in the URL.
File: klea_utils/api/sessions.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.api.sessions.CreateChatPayload(*, chat_id: Annotated[str, _PydanticGeneralMetadata(pattern='^[^:]+$')], title: str = '')[source]¶
Bases:
BaseModel- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class klea_utils.api.sessions.UpdateChatPayload(*, title: str)[source]¶
Bases:
BaseModel- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.api.sessions.create_sessions_router() APIRouter[source]¶
Create an APIRouter for chat session CRUD.
Endpoints:
GET /chat/{user_id}List all chats for the user.
POST /chat/{user_id}Create a new chat. Title is auto-generated from coolname if not provided.
PATCH /chat/{user_id}/{chat_id}Update a chat’s metadata (title).
DELETE /chat/{user_id}/{chat_id}Remove a chat and all associated data.
DELETE /chat/{user_id}Remove all chats, messages, and checkpoints for the user.
Persistent SQLite-backed store for chat session data.
- Manages two tables alongside the LangGraph checkpoint DB:
chat_sessions (chat metadata, listing, and model overrides)
messages (curated Q&A history for chat display)
There is no separate state table. Graph state (plan, goal,
tool_status, …) is read directly from the latest LangGraph checkpoint
via graph.aget_state(thread_id) – the checkpoint DB is the
canonical source and already stores the full deserialised state with no
serialization round-trip.
File: klea_utils/api/sessions_db.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.api.sessions_db.SessionStore(db_path: str | Path)[source]¶
Bases:
objectSQLite-backed persistent store for chat session data.
All public methods are thread-safe. The store auto-creates its schema on first connection.
- Parameters:
db_path – Filesystem path to the SQLite database file.
- add_message(user_id: str, chat_id: str, role: str, content: str, metadata: dict[str, Any] | None = None) None[source]¶
Append a single message to a chat’s history.
- add_messages(user_id: str, chat_id: str, messages: Sequence[dict[str, Any]]) None[source]¶
Append multiple messages atomically.
Each dict must have
roleandcontentkeys, and may have an optionalmetadatakey.
- clear_override(user_id: str, chat_id: str, role: str) None[source]¶
Remove the model override for a single role in a chat.
- create_chat(user_id: str, chat_id: str, title: str = '') None[source]¶
Insert a chat row if it does not already exist.
- get_messages(user_id: str, chat_id: str) list[dict[str, Any]][source]¶
Return all messages for a chat, oldest first.
- get_overrides(user_id: str, chat_id: str) dict[str, dict[str, Any]][source]¶
Return per-role model overrides keyed by role.
Returns
{"rag": {"model": "...", "provider": "..."}, ...}
- rename_chat(user_id: str, chat_id: str, title: str) None[source]¶
Update the display title of a chat.
Session context¶
Session-context projection endpoint.
Returns the session context for a chat – the app-defined projection of
checkpointed graph state via BaseLangGraph.context_snapshot()
(ADR-0032) – so frontends can restore the mode badge / selector on
hydration without waiting for the next streamed query.
Design: the mode lives in checkpointed graph state, which is the single
source of truth (ADR-0030 / ADR-0032). This endpoint reads it from the
checkpoint directly (graph.checkpointer / graph.graph); nothing
is duplicated into the sessions database. An unsent query is not a chat
yet, so a thread with no checkpoint simply reports null context –
the chat entity exists, only its projection is unset, so 200 with
null is returned rather than a 404.
File: klea_utils/api/context.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- klea_utils.api.context.create_context_router() APIRouter[source]¶
Create an APIRouter exposing the graph-level
contextprojection.GET /chat/{user_id}/{chat_id}/contextReturn
{"context": {...}}– the checkpointed session context projected by the app’scontext_snapshothook – or{"context": null}when the thread has no checkpoint yet (a chat that never ran a query) or the graph uses no checkpointer.
Model configuration¶
Per-session model configuration endpoints for runtime model switching.
File: klea_utils/api/models.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- class klea_utils.api.models.ChatModelConfigPayload(*, model: str, api_key: str | None = None, base_url: str | None = None, provider: str | None = None, user_id: str = '')[source]¶
Bases:
BaseModel- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- klea_utils.api.models.create_models_router() APIRouter[source]¶
Create an APIRouter for per-chat model configuration.
GET /chat/{user_id}/{chat_id}/models/overridesReturns stored model overrides for a chat.
GET /chat/{user_id}/{chat_id}/models/activeReturns resolved model config (defaults merged with overrides).
POST /chat/{user_id}/{chat_id}/models/overrides/{role}Stores per-chat model overrides.
SSE streaming¶
Shared SSE streaming client for Klea frontends.
Provides both an async generator (for NiceGUI and TUI) and a synchronous
generator (for Streamlit) that consume the /query/stream SSE endpoint
and yield parsed event dicts.
File: klea_utils/api/sse.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- async klea_utils.api.sse.fetch_active_models(server_url: str, user_id: str, chat_id: str) dict[str, dict[str, str]][source]¶
Fetch the resolved model config per role for a chat.
Calls
GET /chat/{user_id}/{chat_id}/models/activeand returns the merged default + override config dict.- Parameters:
server_url – Base URL of the backend API server.
user_id – Opaque persistent user identifier.
chat_id – Chat conversation identifier.
- Returns:
{"chat": {"model": "...", "provider": "..."}, "guard": ..., "embedding": ...}
- klea_utils.api.sse.fetch_active_models_sync(server_url: str, user_id: str, chat_id: str) dict[str, dict[str, str]][source]¶
Synchronous counterpart of
fetch_active_models().Intended for frontends that cannot use asyncio.
- Parameters:
server_url – Base URL of the backend API server.
user_id – Opaque persistent user identifier.
chat_id – Chat conversation identifier.
- klea_utils.api.sse.format_model_info(info: dict[str, dict[str, str]]) str[source]¶
Build a compact one-line model summary from active models config.
Strips provider prefixes and joins roles, e.g.:
Chat:deepseek-v4-flash | Guard:llama-guard3 | Embedding:bge-m3
- Parameters:
info – The dict returned by
fetch_active_models/fetch_active_models_sync.- Returns:
Empty string if no models are configured.
- async klea_utils.api.sse.stream_events(query: str, chat_id: str, server_url: str, user_id: str = '', extra: dict | None = None) AsyncGenerator[dict, None][source]¶
POST to
/query/streamand yield parsed SSE event dicts.Each yielded dict has at least a
"type"key. Known types:progress {"type": "progress", "node": "<label>"} info {"type": "info", "node": "<label>", "data": {...}} debug {"type": "debug", "node": "<label>", "data": {...}} token {"type": "token", "content": "<chunk>", "node": "<label>"} usage {"type": "usage", "node": "<label>", "data": {...}} context {"type": "context", "data": {...}} (graph-level session context) complete {"type": "complete", "message_for_user": "<text>"} error {"type": "error", "message": "<text>", "error_type": "<class>", "node": "<label>"}
This async generator is intended for NiceGUI and TUI frontends.
- Parameters:
query – User’s query string.
chat_id – Chat conversation identifier.
server_url – Base URL of the backend API server.
user_id – Opaque persistent user identifier.
extra – Optional extra request fields merged into the POST body (e.g. an app-specific
moderequest, ADR-0030).
- klea_utils.api.sse.stream_events_sync(query: str, chat_id: str, server_url: str, user_id: str = '', extra: dict | None = None) Generator[dict, None, None][source]¶
Synchronous counterpart of
stream_events().Intended for frontends that cannot use asyncio. Async frontends (NiceGUI, TUI) should use
stream_events()instead.- Parameters:
query – User’s query string.
chat_id – Chat conversation identifier.
server_url – Base URL of the backend API server.
user_id – Opaque persistent user identifier.
extra – Optional extra request fields merged into the POST body (e.g. an app-specific
moderequest, ADR-0030).
Utilities¶
Utility functions for the Klea API layer.
File: klea_utils/api/utils.py
Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
- async klea_utils.api.utils.check_api_is_ready(url: str, attempts: int | None = None, timeout: float = 180.0)[source]¶
Exponentially back off checking that the API is ready.
- Parameters:
url – Health check endpoint URL
attempts – If set, maximum number of probe attempts (overrides timeout)
timeout – Total wall-clock seconds to keep probing when attempts is unset