Framework: FastAPI
Default port:9999(set via Docker / environment)
- Overview
- Startup & Lifespan
- Agent Modes
- CORS
- REST Endpoints
- WebSocket Endpoint
- Shared Message Contract
- Serialisation Helper
- Error Handling
- Environment Variables
api_server.py is the single entry point for all client–server communication. It exposes:
- A small set of REST endpoints for health checks, config management, MCP server introspection, file upload, and a simple streaming chat.
- A single WebSocket endpoint (
/ws/research) that implements the full interactive research loop powered by the multi-agentOrchestrator.
The server uses FastAPI's lifespan context manager to initialise and tear down all shared state exactly once, rather than on every request.
Application start
│
▼
create_model_backend(config) ← selects backend from MODEL_BACKEND env var
│
▼
create_mcp_registry(config) ← connects to all MCP servers
│
├── memory (MEMORY_SERVER_URL)
├── web_search (SEARCH_SERVER_URL)
├── fetch (FETCH_SERVER_URL)
└── file_handler (FILE_SERVER_URL)
│
├── agent = AdvancedResearchAgent | SelfOptimizingAgent
│ (stored on app.state.agent; used for /mcp/* and /chat)
├── agent_pool = AgentPool.from_single_backend(backend)
│ (shared across all WebSocket sessions)
├── session_store = SessionStore() (in-memory, TTL-based)
└── session_manager = ResearchSessionManager() (owns all background tasks)
│
▼
mcp_registry.start_keepalive() ← 60 s pings to all MCP servers
│
[serve requests]
│
Application shutdown
├── session_manager.shutdown() ← cancels all running research tasks
├── agent_pool.close()
└── mcp_registry.close()
The agent_pool is created once at startup and shared across all WebSocket connections. Each incoming query message creates a fresh, isolated Orchestrator instance (via _make_orchestrator()) so concurrent research sessions never share mutable plan or execution state.
The agent_mode field in Config (set via the AGENT_MODE environment variable, defaulting to "research") selects which agent class is instantiated for REST endpoints:
agent_mode value |
Agent class | Notes |
|---|---|---|
"research" (default) |
AdvancedResearchAgent |
Full research pipeline with parallel step execution and source credibility scoring |
"chat" |
AdvancedResearchAgent |
Same class; mode affects prompt behaviour |
"self-optimization" |
SelfOptimizingAgent |
Experimental agent that analyses stored memories and develops new research strategies |
The agent instance is used only by REST endpoints (/chat, /mcp/*) and the v1 AdvancedResearchAgent codepath. The WebSocket /ws/research endpoint always uses a fresh Orchestrator per session regardless of AGENT_MODE.
The server enables CORS for all origins (*) with all methods and headers allowed. This is intentionally permissive for local development. For production deployments, replace the wildcard with an explicit list of allowed frontend origins.
Health ping. Returns the API name, version string, and running status.
Response
{
"name": "Research Agent API",
"version": "1.0.0",
"status": "running"
}Detailed health check. Confirms the agent has been initialised.
Response
{
"status": "healthy",
"timestamp": "2026-03-06T14:22:01.123456+00:00",
"agent_initialized": true
}503 — returned if the agent failed to initialise during startup (rare; typically a missing MCP connection).
Lists the names of all MCP servers that are currently registered and connected.
Response
{
"servers": ["memory", "web_search", "fetch", "file_handler"]
}503 — agent not initialised.
Lists every tool available across all connected MCP servers. The response is the raw tool-spec list used internally for model tool-calling.
Response
{
"tools": [
{
"type": "function",
"name": "web_search",
"description": "Search the web for a query",
"parameters": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }
},
...
]
}503 — agent not initialised.
Uploads a file and saves it under backend/data/uploaded_files/<filename>. The file is stored on the server's local filesystem and is then accessible to the file_handler MCP server.
Request — multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file |
binary | ✓ | The file to upload |
Response
{
"status": "success",
"filename": "my_document.pdf"
}503 — agent not initialised.
Returns a snapshot of the current Config object (all fields, including model names and server URLs). Sensitive values such as API keys are included — restrict access to this endpoint in production.
Response
{
"config": {
"model_backend": "openai",
"openai_model": "gpt-5.2",
"ollama_model": "nemotron-3-nano",
"max_iterations": 10,
"default_temperature": 0.7,
...
}
}Updates the active agent's model backend and/or model name at runtime. Reinitialises the AdvancedResearchAgent with the new configuration — the Orchestrator is not reinitialised (restart the server to apply backend changes to the Orchestrator).
Request body (application/json) — ConfigUpdate
| Field | Type | Required | Description |
|---|---|---|---|
model_backend |
string |
✗ | One of openai, ollama, aws, azure, gcp, huggingface |
model |
string |
✗ | Model name to use for the selected backend |
Response
{
"status": "success",
"message": "Configuration updated",
"config": {
"model_backend": "openai",
"model": "gpt-5.2"
}
}500 — invalid configuration or failed to reinitialise the agent.
Streaming chat endpoint for direct conversational interaction with the agent, bypassing the full research pipeline. Useful for quick questions or testing model connectivity.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
content |
string |
✓ | The user's message |
Response — text/plain streamed in real-time as the model generates tokens.
The latest advancements in renewable energy include...
503 — agent not initialised.
All graph endpoints query the KnowledgeGraph layer of AsyncLongTermMemory. They return {"message": "LongTermMemory not available"} if Neo4j is not connected.
Returns entity, relationship, community, contradiction, document, claim, and hierarchy counts.
Semantic entity search across all typed node labels.
| Param | Type | Default | Description |
|---|---|---|---|
query |
string |
"" |
Semantic search query. If empty, returns most recent entities. |
limit |
int |
10 |
Max results (1–100) |
include_hierarchy |
bool |
false |
Include IS_A ancestor names |
node_type |
string |
"" |
Filter by entity type (comma-separated: person,technology) |
Traverse typed relationships (CAUSES, ENABLES, USES, etc.) from a named entity.
| Param | Type | Default | Description |
|---|---|---|---|
entity |
string |
"" |
Seed entity name |
max_hops |
int |
2 |
Traversal depth (1–4) |
min_confidence |
float |
0.0 |
Post-filter by confidence |
List all community cluster summaries.
Return CONTRADICTS edges, optionally filtered to a named entity.
| Param | Type | Default | Description |
|---|---|---|---|
entity |
string |
"" |
Filter by entity name |
limit |
int |
20 |
Max results (1–100) |
Return Document nodes linked to entities via SOURCED_FROM edges.
| Param | Type | Default | Description |
|---|---|---|---|
entity |
string |
"" |
Filter by entity name |
Find shortest paths between two named entities.
| Param | Type | Default | Description |
|---|---|---|---|
source |
string |
(required) | Source entity name |
target |
string |
(required) | Target entity name |
max_depth |
int |
4 |
Max path length |
Return entities and relationships created during a specific research session.
Search claims by semantic query, entity, or status.
| Param | Type | Default | Description |
|---|---|---|---|
query |
string |
"" |
Semantic search query |
entity |
string |
"" |
Filter by linked entity name |
status |
string |
"" |
Filter by claim status (supported, disputed, unverified, retracted) |
limit |
int |
20 |
Max results (1–100) |
Update a claim's status.
| Param | Type | Default | Description |
|---|---|---|---|
status |
string |
(required) | New status value |
Search documents by semantic query, optionally filtered.
| Param | Type | Default | Description |
|---|---|---|---|
query |
string |
"" |
Semantic search query |
doc_type |
string |
"" |
Filter by document type |
min_credibility |
float |
0.0 |
Minimum credibility score |
limit |
int |
20 |
Max results (1–100) |
Prune stale graph elements. Runs in dry-run mode by default.
| Param | Type | Default | Description |
|---|---|---|---|
min_confidence |
float |
0.1 |
Remove edges below this confidence |
max_age_days |
int |
180 |
Remove edges not confirmed in this many days |
dry_run |
bool |
true |
When true, only count — do not delete |
Powered by the multi-agent Orchestrator. Each query message creates a new, isolated research session with its own Orchestrator instance so concurrent connections do not share state. The session ID is returned immediately so the client can reconnect if the connection drops.
The full pipeline:
SearchAgent → AnalystAgent → LoopAgent (QA) → [retry if contradictions] → ReportComposer
Three-phase flow:
- Plan — generate a
ResearchPlanfor the user's query. - Execute — run each plan step through the sub-agent pipeline with tool-calling.
- Synthesise — multi-pass synthesis via
ReportComposer; Markdown document emitted asreportevent.
See ORCHESTRATOR.md for the complete architecture reference.
Both WebSocket endpoints share the same JSON message protocol.
All messages sent from the client to the server are JSON objects with a type field.
Starts a new research session. Triggers plan generation.
{
"type": "query",
"content": "What are the long-term cardiovascular effects of GLP-1 agonists?"
}| Field | Type | Required | Description |
|---|---|---|---|
content |
string | ✓ | The research question |
Approves the pending plan and begins execution + synthesis.
{
"type": "approve_plan",
"planId": "3f7a2b1c-..."
}| Field | Type | Required | Description |
|---|---|---|---|
planId |
string | ✓ | UUID from the plan event |
Requests a revised plan incorporating free-text feedback. The server regenerates the plan and emits a new plan event.
{
"type": "modify_plan",
"planId": "3f7a2b1c-...",
"feedback": "Please add a step that searches specifically for meta-analyses and RCTs."
}| Field | Type | Required | Description |
|---|---|---|---|
planId |
string | ✓ | UUID of the plan to modify |
feedback |
string | ✓ | Free-text description of desired changes |
Rejects the pending plan and resets agent state. The client may then send a new query.
{
"type": "deny_plan",
"planId": "3f7a2b1c-..."
}| Field | Type | Required | Description |
|---|---|---|---|
planId |
string | ✓ | UUID of the plan to reject |
Reconnects to an existing research session by session_id. The server replays the full event log from the beginning, then streams any live events still being produced by the background execution task. If the session has already completed, the client receives the full replay immediately.
{
"type": "resume",
"session_id": "3f7a2b1c-..."
}| Field | Type | Required | Description |
|---|---|---|---|
session_id |
string | ✓ | The session ID returned in a prior session_created event |
All messages sent from the server to the client have at minimum type and message fields.
interface ServerMessage {
type: string;
message: string;
data?: Record<string, any>; // type-dependent payload
error?: string; // present on error types
plan?: ResearchPlan; // present on type="plan"
}Sent immediately after a query message, before plan generation begins. Contains the session_id the client should persist for reconnection.
{
"type": "session_created",
"session_id": "3f7a2b1c-..."
}Sent after a successful resume before the event replay begins.
{
"type": "session_resumed",
"session_id": "3f7a2b1c-...",
"state": "executing",
"complete": false,
"event_count": 42
}A progress or informational update. May include a data object with step-level detail. Emitted frequently throughout all phases.
{
"type": "status",
"message": "[QA] Auditing findings for step 2…"
}Some status events carry additional QA data:
{
"type": "status",
"message": "[QA] 2 contradiction(s) flagged in step 2. Routing back for investigation…",
"data": {
"contradictions": [
{
"id": "...",
"source_a": "https://...",
"claim_a": "Drug X reduces MACE by 14%",
"source_b": "https://...",
"claim_b": "Drug X reduces MACE by 9%",
"context": "Primary efficacy endpoint",
"resolved": false,
"flagged_at": "2026-03-06T14:30:00+00:00"
}
]
}
}The research plan is ready for the user to review. Sent after query or modify_plan.
{
"type": "plan",
"message": "Research plan ready. Awaiting approval.",
"plan": {
"id": "3f7a2b1c-...",
"goal": "Assess long-term cardiovascular effects of GLP-1 agonists.",
"steps": [
{ "id": 1, "name": "Step 1", "description": "Search for landmark RCTs…", "status": "pending" },
{ "id": 2, "name": "Step 2", "description": "Retrieve meta-analyses…", "status": "pending" }
]
}
}A plan step has begun execution.
{
"type": "step_start",
"message": "Step 1: Search for landmark RCTs…",
"data": {
"step": { "id": 1, "name": "Step 1", "description": "...", "status": "in_progress" }
}
}A plan step finished successfully.
{
"type": "step_complete",
"message": "Step 1 complete.",
"data": {
"step": { "id": 1, "status": "completed", ... },
"result": "Found 4 landmark RCTs: SUSTAIN-6, LEADER, EMPA-REG, DECLARE-TIMI 58..."
}
}A plan step failed. Research continues with remaining steps.
{
"type": "step_failed",
"message": "Step 2 failed",
"error": "Connection timeout to fetch MCP server"
}All plan steps have finished executing. Synthesis is about to begin.
{
"type": "research_complete",
"message": "All steps executed. Proceeding to synthesis…"
}The final structured research synthesis from AdvancedResearchAgent. Follows the SynthesisResult shape.
{
"type": "synthesis",
"message": "Synthesis complete.",
"data": {
"summary": "GLP-1 agonists demonstrate consistent MACE reduction…",
"key_insights": ["..."],
"patterns": ["..."],
"recommendations": ["..."],
"creative_applications": ["..."],
"knowledge_gaps": ["..."]
}
}The final Markdown research report from the ReportComposer. Sent as the last event of a v2 session.
{
"type": "report",
"message": "Research report complete.",
"data": {
"document": "# GLP-1 Agonists: Cardiovascular Outcomes\n\n## Executive Summary\n..."
}
}Acknowledges the plan rejection and suggests next steps.
{
"type": "plan_denied",
"message": "Understood. Consider narrowing your query to a specific drug class or trial population…"
}Acknowledgements for in-flight control actions.
{ "type": "research_paused", "message": "Research paused. Send 'resume' to continue." }
{ "type": "research_resumed", "message": "Research resumed." }
{ "type": "research_stopped", "message": "Research stopped." }An error occurred. The WebSocket remains open; the client may send a new query.
{
"type": "error",
"message": "planId is required to approve a plan."
}make_serializable(obj) is a recursive converter that ensures any research domain object can be safely passed to websocket.send_json().
| Input type | Action |
|---|---|
ResearchStep or ResearchPlan |
Calls .to_dict(), then recurses |
dict |
Recurses over all values |
list / tuple |
Recurses over all items |
Anything else (str, int, float, bool, None) |
Passes through unchanged |
This is applied to every outbound WebSocket message before transmission.
| Scenario | Behaviour |
|---|---|
| Agent not initialised | REST endpoints return 503; WebSocket sends type="error" then closes |
Empty query content |
WebSocket sends type="error", connection remains open |
Missing planId |
WebSocket sends type="error", connection remains open |
modify_plan failure |
Caught, sends type="error", connection remains open |
| All steps failed | Orchestrator emits type="error" after final failure |
| Unhandled exception in WS loop | Logged, sends type="error" to client |
| WebSocket disconnect | Background execution task continues; client can reconnect via resume |
All variables are read via the Config Pydantic model at startup. The most operationally relevant ones for the API server are:
| Variable | Default | Description |
|---|---|---|
MODEL_BACKEND |
ollama |
Model provider: openai, ollama, aws, azure, gcp, huggingface |
AGENT_MODE |
research |
Agent class for REST endpoints: research, chat, self-optimization |
LOG_LEVEL |
20 (INFO) |
Python logging level integer |
MEMORY_SERVER_URL |
http://localhost:9494/mcp |
MCP memory server URL |
SEARCH_SERVER_URL |
http://localhost:9393/mcp |
MCP web search server URL |
SCRAPER_SERVER_URL |
http://localhost:9292/mcp |
MCP web scraper server URL |
FILE_SERVER_URL |
http://localhost:9191/mcp |
MCP file handler server URL |
OPENAI_API_KEY |
— | Required when MODEL_BACKEND=openai |
OPENAI_MODEL |
gpt-5.2 |
OpenAI model name |
OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama server base URL |
OLLAMA_MODEL |
nemotron-3-nano |
Ollama model name |
AWS_MODEL |
global.anthropic.claude-sonnet-4-6 |
Bedrock model ID |
AWS_BASE_URL |
— | Required when MODEL_BACKEND=aws |
AWS_API_KEY |
— | Required when MODEL_BACKEND=aws |
AZURE_OPENAI_ENDPOINT |
— | Required when MODEL_BACKEND=azure |
AZURE_OPENAI_API_KEY |
— | Required when MODEL_BACKEND=azure |
AZURE_OPENAI_DEPLOYMENT |
— | Azure deployment name |
GCP_ENDPOINT |
https://us-central1-aiplatform… |
GCP Vertex AI endpoint |
GCP_MODEL |
gemini-2.5-pro |
Vertex AI model name |
GCP_API_KEY |
— | Required when MODEL_BACKEND=gcp |
MAX_ITERATIONS |
3 |
Max tool-calling iterations per research step |
MAX_QA_RETRIES |
2 |
Max LoopAgent contradiction-retry cycles per step |
MAX_TOKENS |
4096 |
Max output tokens per generation call |
DEFAULT_TEMPERATURE |
0.7 |
Default generation temperature |
MAX_SOURCES_PER_QUERY |
5 |
Max sources retrieved per search step |
MAX_DEPTH |
2 |
Max recursive scraping depth |
ENABLE_PARALLEL_EXECUTION |
True |
Run independent plan steps concurrently |
MCP_KEEPALIVE_INTERVAL |
60 |
Seconds between MCP server keepalive pings |