ChatAgents is structured as two cooperating services — a Streamlit frontend and a FastAPI backend — running from the same Docker image. This split keeps the chat UI decoupled from agent execution: the frontend owns rendering and user interaction, while the backend owns all LLM calls, tool invocations, and conversation state. Streaming NDJSON events bridge them in real time, so users see tokens and tool-call cards appear as the agent works rather than waiting for a complete response.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/EllisYuan/ChatAgents/llms.txt
Use this file to discover all available pages before exploring further.
Service Overview
Streamlit Frontend
Port 8501 — Renders the chat interface, sidebar API-key management, and tool-call visualization cards. Communicates with the backend exclusively via HTTP streaming (
POST /stream_agent). Maintains lightweight session state in Streamlit’s own session store.FastAPI Backend
Port 8080 — Owns the LangGraph
WebAgent, the /stream_agent streaming endpoint, and all session CRUD routes (/api/sessions). Validates API keys, builds the ReAct graph per request, and persists conversation state through a shared MemorySaver checkpointer.Request Lifecycle
User types a message
The user submits a query in the Streamlit chat input. The frontend packages the message together with session metadata and sends it to the backend.
Streamlit POSTs to /stream_agent
The frontend sends an HTTP API keys are passed as request headers (
POST to /stream_agent with a JSON body matching the AgentRequest model:X-Tavily-Key, X-Claude-Key, etc.) or read from environment variables as a fallback.FastAPI validates API keys
The backend calls
check_api_key() on the Tavily key (format and authorization check) and verifies that the requested LLM provider key is present. If validation fails, a 401 or 400 HTTPException is raised before any LLM cost is incurred.WebAgent.build_graph() creates the ReAct agent
Based on Internally,
agent_type, the backend selects the correct prompt (get_simple_prompt() for Fast, get_reasoning_prompt() for Deep) and calls WebAgent.build_graph() with the main LLM, a fast Claude Haiku summarizer LLM, the Tavily API key, and the mode string — build_graph() constructs the three Tavily tools internally:build_graph() configures TavilySearch, TavilyExtract, and TavilyCrawl with mode-appropriate parameters, then calls create_react_agent() with the shared MemorySaver checkpointer.astream_events() runs the agent and yields NDJSON
The backend drives the agent asynchronously using LangGraph’s
astream_events() API. Each LangGraph event is mapped to one of four NDJSON event types and written to the streaming response:Streamlit renders tokens and tool cards in real time
The frontend reads the chunked response line-by-line.
chatbot events are appended to the chat bubble as tokens arrive. tool_start and tool_end events render collapsible tool-call cards showing the tool name, type, input parameters, and source links returned by the tool.Session saved to JSON after stream ends
Once the stream is complete (inside the generator’s
finally block), the backend calls session_manager.save_session(), which writes the full exchange — user message, assistant response, and any tool calls — to data/sessions/<thread_id>.json. If no session file exists yet, the first user message is used to auto-generate a title.Conversation Memory
Multi-turn context is maintained by a LangGraphMemorySaver checkpointer. The checkpointer is created once at application startup and stored on app.state:
build_graph() passes self.checkpointer to create_react_agent(). LangGraph then uses the thread_id from the request config to read and write checkpoint snapshots — so the agent picks up the full message history from prior turns automatically, without the frontend needing to replay messages.
The agent graph is re-built on every request (new LLM instance, new tool instances), but the single
MemorySaver instance lives for the entire application lifetime. This means conversation context survives across requests, while each request gets fresh LLM and tool configuration (model choice, API key, mode parameters).NDJSON Event Reference
Every line the backend streams is a JSON object terminated by\n. The Streamlit frontend dispatches on the type field:
| Event type | Emitted by | Payload fields |
|---|---|---|
chatbot | on_chat_model_stream handler | type, content — one text chunk from the LLM’s final answer |
tool_start | on_tool_start handler | type, tool_name, tool_type (search/extract/crawl), operation_index, content (serialized input params) |
tool_end | on_tool_end handler | type, tool_name, tool_type, operation_index, content (serialized tool output, including source URLs and favicons) |
error | Exception handler in generator | type, content — human-readable error message |
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | Streamlit 1.32+ | Python-native chat UI, sidebar controls, real-time streaming render |
| Backend | FastAPI 0.109+ | Async HTTP server, streaming endpoint, session CRUD API |
| Agent orchestration | LangGraph | ReAct agent graph, MemorySaver checkpointer, astream_events streaming |
| Primary LLM | Claude (Haiku / Sonnet / Opus) | Agent reasoning; OpenAI and Groq interfaces also supported |
| Web tools | Tavily (langchain-tavily) | Search, extract, and crawl the live web |
| Session storage | JSON files (data/sessions/) | Lightweight persistence; no database dependency |
| Containerization | Docker + Docker Compose | Single-image, two-service deployment |
| Configuration | python-dotenv | .env-based API key management |