Skip to main content

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:

  1. Think — ask the LLM (with tool schemas) for the next action.
  2. 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.
  3. Observe — tool results are appended to conversation memory.
  4. Respond — when the LLM stops calling tools, the final answer is returned.

Key mechanics:

  • Step budgetAgentConfig.max_steps (default 15). On the final step tool calling is disabled and the model is forced to answer.
  • Streamingrun_streaming() yields AgentStep objects (thinking, acting, observing, responding, …) as they happen; the FastAPI router serialises them as SSE events.
  • Cancellationloop.cancel() sets an event that the loop checks at every step boundary.
  • Spatial contextrun() / run_streaming() accept location (map viewport centre), selected_feature and a GeoJSON geometry attachment; 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-process FastMCP server (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.