Deep dive into the conversation orchestration layer: tenant registry, session lifecycle, LangGraph ReAct agent, handoff mechanics, button rules, and deterministic response formatting.
Each tenant is a self-contained directory under tenants/ with its own config, tools, prompts, and button rules.
The registry lazy-loads tenant runtimes on first access to prevent circular imports.
Tenant Directory Structure
| File | Purpose |
|---|---|
tenants/tsg/config.py |
AgentConfig: model name, temperature, session expiry, greeting words, timezones |
tenants/tsg/prompts.py |
System prompt template + per-tool description strings |
tenants/tsg/button_rules.py |
Declarative ButtonRule list — attach UI buttons after tool execution |
tenants/tsg/formatters.py |
Deterministic response formatters that replace LLM text for critical outputs |
tenants/tsg/tools/ |
StructuredTool implementations: document_tools, operator_tools, service_tools |
tenants/tsg/services/ |
Business logic layer: document validation, service management |
Adding a New Tenant
tenants/tsg/ as a template. Rename to tenants/{new_id}/.config.py.prompts.py.tools/__init__.py.
SessionManager handles creation, expiry, reset, and handoff resume.
Sessions are stored in MongoDB and keyed by phone number + tenant.
| Trigger | Action | Details |
|---|---|---|
| First message from number | Create session | New session in MongoDB, fresh LangGraph state |
| 30 minutes idle | Expire session | Configurable via session_expiry_minutes in AgentConfig |
| Greeting word detected | Reset session | Words like "hello", "hi", "hola", "start" trigger full state wipe |
| Handoff signal | Transfer to human | User requested agent OR 5+ consecutive no-tool responses |
| Agent returns conversation | Resume with history | Full agent conversation injected into LangGraph state before AI takes over |
Central orchestrator at core/engine/engine.py. Receives a NormalizedMessage and returns an EngineResponse.
await graph.ainvoke(agent_state, config) — runs the ReAct loop (chatbot → tools → chatbot...) until completion or handoff.
Three-node graph at core/agent/graph.py implementing the ReAct pattern with a conditional router.
After tools node completes, control returns to chatbot (loop). Router exits to END when no tool calls remain, or to handoff when escalation is needed.
Router Logic (route_after_chatbot)
| Condition | Route To | Details |
|---|---|---|
| Tool calls detected in LLM response | "tools" node |
Execute the requested tools, then loop back to chatbot |
needs_handoff flag set OR 5+ consecutive no-tool responses |
"handoff" node |
Route to human agent queue. Infinite loop guardrail. |
| No tool calls and no handoff signal | END |
Conversation turn complete, return response |
Critical tool outputs bypass LLM interpretation. Each tool can register a formatter function that produces the exact response text.
Declarative rules that attach interactive buttons to responses based on conditions. Defined per tenant in button_rules.py.
Seamless transition between AI and human agents with full context preservation.
Custom checkpointer at core/engine/mongodb_checkpointer.py that persists full LangGraph state to MongoDB.
Enables session resumption across process restarts and provides full conversation replay for debugging.
| Feature | Details |
|---|---|
| Persistence | Full graph state (messages, user_data, tool results) serialized to lg_sessions collection |
| Session Resume | On reconnect, loads last checkpoint and continues conversation from exact point |
| History Replay | All intermediate states stored — enables step-by-step conversation replay for debugging |
| Keying | Checkpoints keyed by session_id (derived from phone_number + tenant_id) |