ContactOS — Platform Architecture Overview

AI-powered multi-tenant customer service platform. Orchestrates conversations across WhatsApp, web, and voice channels using LangGraph ReAct agents with deterministic tool execution, real-time analytics, and seamless human handoff.

1. Tech Stack

ContactOS is a full-stack Python + React application with MongoDB persistence and Redis for real-time state.

Backend Python / FastAPI
FrameworkFastAPI 0.115+ with Uvicorn ASGI
AI EngineLangGraph 0.2+ (ReAct pattern)
LLMClaude Haiku 4.5 (default), configurable per tenant
DatabaseMongoDB via Motor 3.3+ (async)
CacheRedis 5.0+ for sessions & real-time state
ValidationPydantic 2.5+ with SQLAlchemy 2.0+
LangChain Core 0.3+ LangChain OpenAI LangChain Anthropic Twilio Google APIs Boto3
Frontend React / TypeScript
FrameworkReact 19.2 + TypeScript 5.9
BuildVite 7.3
StateTanStack React Query 5.90 + Zustand 5.0
StylingTailwind CSS v4
ChartsECharts 6.0 (via @datastudios/viz)
Real-timeWebSocket with auto-reconnect (3s)
React Router 7.13 Lucide Icons i18n (ES/EN) JWT Auth

2. Five-Layer Architecture

Every message flows through five layers: channel ingestion, engine orchestration, LangGraph intelligence, tool execution, and channel delivery.

Layer 1 Channel Adapters api/channels/
WhatsApp Twilio webhook
Web Chat HTTP POST
Voice AI Future
NormalizedMessage Unified contract
Layer 2 ConversationEngine core/engine/engine.py
Session Mgr Get or create
Reset Check Greeting words
Handoff Resume Inject agent history
Invoke Graph LangGraph ainvoke()
Post-Process Buttons + formatters
Layer 3 LangGraph Agent core/agent/graph.py — ReAct pattern
Chatbot Node LLM response or tool calls
route_after_chatbot() Router logic
Tools Node Execute structured tools
Handoff Node Route to live agent
Layer 4 Tool Execution tenants/{tenant}/tools/
validate_document ID/email format
confirm_operator Route to queue
confirm_services Customer services
extract_services Parse from docs
modify_services Update details
write_services Persist to DB

Each tool is a LangChain StructuredTool with Pydantic input schema, deterministic formatter, and button rules. Tool set is tenant-specific — new tenants define their own tools.

Layer 5 Channel Senders core/channels/sender.py
EngineResponse text + buttons + metadata
WhatsApp Twilio API
Web HTTP JSON response
Voice TTS Future

3. Key Data Contracts

Three core Pydantic models define the boundaries between layers.

// NormalizedMessage — Channel → Engine { "phone_number": "+1234567890", "text": "Hola, necesito ayuda", "tenant_id": "tsg", "channel": "whatsapp", "message_id": "SM...", "timestamp": "2026-03-18T14:30:00Z" } // AgentState — flows through all graph nodes { "messages": [BaseMessage...], "tenant_id": "tsg", "channel": "whatsapp", "session_id": "uuid", "phone_number": "+1234567890", "user_data": {}, "needs_handoff": false, "conversation_id": "mongo_id" } // EngineResponse — Engine → Channel { "text": "Formatted response...", "buttons": [{"label": "Confirm", "action": "confirm"}], "needs_handoff": false, "session_ended": false }
1 NormalizedMessage

Unified contract from all channel adapters. Strips channel-specific details (Twilio SIDs, HTTP headers) into a clean format the engine understands.

2 AgentState

TypedDict that flows through every LangGraph node. Contains conversation history (auto-merged), tenant selector, session ID, and handoff flags.

3 EngineResponse

Output contract with response text, UI buttons (attached by ButtonRules), and handoff/session flags. Channel senders use this to deliver via the original channel.

4. Database Model

MongoDB (contactos database) with Motor async driver. All collections scoped by tenant_id.

Collection Purpose Key Fields
lg_sessions LangGraph checkpoints — full graph state persistence session_id, checkpoint_data, created_at
conversations Conversation records with status tracking tenant_id, phone_number, status, messages
messages Full message log with sentiment scores conversation_id, role, text, sentiment
contacts Customer directory phone_number, name, email, metadata
agents Human agent roster and availability agent_id, name, status, assigned_conversations
tenants Tenant configuration and metadata tenant_id, config, active_since
ai_insights Auto-generated analytics summaries tenant_id, period, content, generated_at

Accessed via DatabaseManager factory with per-collection DAOs: ConversationDB, MessageDB, AgentDB, ContactDB, AnalyticsDB, TenantDB. Connection configured via MONGO_URI or component env vars.

5. Multi-Tenancy

Lazy-loaded tenant registry. Each tenant is a self-contained directory with its own config, tools, prompts, and button rules.

Config AgentConfig
Per-tenant AI behavior configuration.
Modelclaude-haiku-4-5-20251001
Temp0.3 (deterministic)
Expiry30 min idle timeout
Greetingshello, hi, hola, start...
TimezoneSource + display TZ
Tools StructuredTools
LangChain StructuredTool with Pydantic schemas. Each tool has a deterministic formatter that replaces LLM interpretation.
validate_document confirm_operator confirm_services cancel_services extract_services modify_services write_services
UI Rules ButtonRules
Declarative rules that attach UI buttons after tool execution. Match on tool results or text patterns.
MatchPattern/text/tool-result
Actionconfirm, edit, cancel
FormatList<ButtonRule>

6. Frontend Dashboard

React 19 SPA with a three-column layout: conversation list, chat thread, and customer detail panel.

Left 25% ConversationPanel
Filterable conversation list with tab navigation.
TabsAll, Mine, Unassigned, AI
PollingReact Query (10s)
SearchPhone number, status filter
Center 50% ChatPanel
Real-time message thread with button actions and input.
RolesUser, Bot, System, Agent
Real-timeWebSocket /ws/chat (3s reconnect)
PollingReact Query (5s fallback)
Right 25% DetailPanel
Customer metadata, conversation status, and agent actions.
InfoName, phone, email, history
ActionsAssign, transfer, return to AI
AgentsStatus list with availability

7. Infrastructure & Deployment

Docker Compose orchestration with environment-variable-driven configuration.

Infrastructure Components
FastAPI Backend
Port 3405 · Uvicorn ASGI
React Frontend
Port 5174 (dev) · Vite
📋
MongoDB
Port 27017 · Motor async
Redis
Sessions & real-time state
🔌
WebSocket
/ws/chat · same port as backend
Environment Variable Purpose Required
MONGO_URI MongoDB connection string (or MONGO_USER + MONGO_PASSWORD + MONGO_ENDPOINT) Yes
ANTHROPIC_API_KEY Claude API key for LLM inference Yes
REDIS_URL Redis connection for sessions and real-time state No (defaults localhost)
WHATSAPP_TOKEN Twilio auth token for WhatsApp channel No (WhatsApp only)
VERIFY_TOKEN Webhook verification token No (WhatsApp only)
OPENAI_API_KEY Optional OpenAI key for alternative LLM providers No

8. Testing Strategy

Four-level test pyramid with pytest markers. No real LLM calls in L1-L3.

Level Marker Scope Tools
L1 Unit unit Pure functions, no IO ScriptedChatModel (mock LLM)
L2 Contract contract Interface contracts RecordingToolNode
L3 Flow flow Multi-turn conversation flows Real graph + mocked tools
L4 Integration integration End-to-end with backend + Redis Real backend, real tools

9. Notable Design Decisions

Key architectural choices that shape the platform.

Deterministic Formatters
Tool execution results are formatted deterministically — the LLM never interprets critical outputs like service confirmations. This prevents hallucination on high-stakes data.
Handoff Resume
When a conversation returns from a human agent, the AI receives the full agent conversation history injected into state. Enables seamless context continuation.
Infinite Loop Guardrail
Counts consecutive no-tool AI responses. After 5 iterations without a tool call, forces handoff to a human agent to prevent loops.
MongoDB LangGraph Checkpointer
Full conversation state persisted to MongoDB via a custom checkpointer. Enables session resumption, history replay, and debugging across process restarts.
Spanish-First Localization
System prompts, tool outputs, and UI strings default to Spanish. Centralized locale layer at core/localization/ supports future language expansion.
Lazy-Loaded Tenants
Tenant configurations load on first access via _LazyDict to prevent circular imports. Adding a new tenant only requires a tenants/{id}/ directory.