Architecture
┌────────────────────────────────────────────┐
FastAPI app │ fastapi_integration │
(your project) ───► │ create_flow_router create_agent_router │
│ create_tools_router websocket endpoint │
└───────────────┬────────────────────────────┘
│
┌───────────────▼────────────────────────────┐
│ AgentLoop (agent/) │
│ ReAct: think → act → observe → respond │
│ run() · run_streaming() · cancel() │
├──────────────┬─────────────────────────────┤
│ LLMProvider │ MCPClient │
│ ollama/ │ in-memory (FastMCP) or │
│ openai/ │ HTTP transport │
│ base.py │ (mcp_client.py) │
└──────┬───────┴─────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌─────▼──────────────┐
│ memory/ │ │ core/ tool │
│ Conversation│ │ registry + @ai_tool│
│ Memory + │ │ orchestrator, │
│ long-term │ │ breaker & retries │
└─────────────┘ └────────────────────┘
│
┌──────▼──────────────────────────────────────┐
│ Cross-cutting: exceptions, observability │
│ (hooks, metrics, logging), frontend_tools │
└─────────────────────────────────────────────┘
The ReAct loop (agent/loop.py)
AgentLoop drives a bounded reasoning cycle:
- Think — ask the LLM (with tool schemas) for the next action.
- Act — execute the requested tool(s) via the
MCPClient(or a frontend tool — see below). Tool calls are validated against schemas; invalid calls are reported back to the LLM. - Observe — tool results are appended to conversation memory.
- Respond — when the LLM stops calling tools, the final answer is returned.
Key mechanics:
- Step budget —
AgentConfig.max_steps(default 15). On the final step tool calling is disabled and the model is forced to answer. - Streaming —
run_streaming()yieldsAgentStepobjects (thinking,acting,observing,responding, …) as they happen; the FastAPI router serialises them as SSE events. - Cancellation —
loop.cancel()sets an event that the loop checks at every step boundary. - Spatial context —
run()/run_streaming()acceptlocation(map viewport centre),selected_featureand a GeoJSONgeometryattachment; they are injected as system messages so the model can answer "what is this area?" style questions.
Tools (core/, agent/mcp_client.py)
MCPClient— generic MCP client. In direct mode it wraps an in-processFastMCPserver (no network); in HTTP mode it talks to an MCP server over HTTP. Tool lists and schemas are exposed for the LLM.@ai_tool(decorators/) — register plain Python functions into the tool registry with automatic Pydantic validation, plus circuit breaker and retry policy per tool.AgentOrchestrator(core/orchestrator.py) — executes single or batch tool requests through the registry with circuit-breaker protection.
LLM providers (llm/)
LLMProvider (abstract) → OllamaProvider / OpenAI implementations.
The Ollama provider supports streaming, embeddings, and a text-based tool
calling fallback (extract_tool_calls_from_text, validate_tool_call) for
models that don't speak native function calling.
Memory (memory/)
ConversationMemory keeps a sliding window of chat/tool messages for a run
and exposes add_user_message, add_tool_result, add_system_message,
get_messages. Optional long-term memory (memory_service) can inject
retrieved context based on the current message.
Human-in-the-loop (agent/approval_registry.py)
Tools can be flagged as approval-gated — via requires_approval=True on
@ai_tool, via MCPClient(approval_tools={"tool_name"}) for MCP tools (which
surfaces x_requires_approval: true on the tool schema), or an explicit
x_requires_approval entry in an MCP tool's schema. When the loop reaches
such a call it yields an AWAITING_APPROVAL step and blocks on an
asyncio.Event registered in ApprovalRegistry. The client resolves it via
POST /agent/approval/{id}; the loop then either executes (approved) or
reports the rejection (rejected) to the LLM and continues. Requests
auto-reject after approval_timeout_s. The registry is in-process
(single worker); swap it for Redis pub/sub in multi-worker deployments.
Frontend tools (frontend_tools.py)
@frontend_tool(event="…") marks a tool as a UI command rather than a
computation. The loop emits a FRONTEND_TOOL step with the event name +
arguments; the React useAgent hook delivers it to your onFrontendEvent
callback so you can zoom_to, draw_geometry, show_toast, etc. Built-in
GIS events are pre-registered (see Adding tools).
Observability (observability/)
HookManager/AgentHooks expose lifecycle callbacks (step start/end, tool
calls, errors), MetricsCollector can export to Prometheus (optional dep),
and AgentLogger/LogContext provide structured logs.