Inbound Processing
This page covers the full pipeline from an inbound Twilio message to persisted suggestions in the database.
Full Flow
Section titled “Full Flow”Twilio → POST /twilio/events (inbound-message.received) │ ├─ Resolve phone_number from `to` field ├─ Resolve or create contact from `from` field ├─ Create message record (direction=inbound, status=received) ├─ tokio::spawn → update contact.last_contacted_at │ └─ tokio::spawn → handle_inbound_message_for_text_agent( org_id, contact_id, message_id, phone_number_id, channel, body ) [non-blocking — SMS receipt never waits for AI]AI processing is fully async. The Twilio webhook returns immediately after persisting the message.
Agent Resolution
Section titled “Agent Resolution”1. SELECT * FROM contact_text_agent WHERE contact_id = ? AND channel = ? → found: use this agent + its auto_reply setting
2. SELECT * FROM text_agent_default_channel WHERE organization_id = ? AND channel = ? → found: use this agent, auto_reply = false
3. Not found → return Ok(()) // silently skipContext Building
Section titled “Context Building”File: src/mods/text_agent/services/build_text_agent_context_service.rs
The context service assembles the system prompt and conversation history passed to the AI model.
System Prompt
Section titled “System Prompt”| Section | Source |
|---|---|
| Agent name + purpose | text_agent.name + text_agent.purpose |
| Organization context | build_organization_context() — org profile (name, industry, hours, etc.) |
| Contact name | contact.first_name + contact.last_name |
| Contact phones | All rows in contact_phone |
| Contact tags | Joined from contact_contact_tag + contact_tag |
| Contact notes | All contact_note rows, ASC by created_at |
| Output instructions | ”Generate exactly 3 distinct, natural reply options with confidence scores 0.0–1.0” |
Contact notes include any system-generated notes from past calls (e.g., call summaries from the analyzer module), giving the agent awareness of the contact’s full history.
Conversation Messages
Section titled “Conversation Messages”SELECT * FROM messageWHERE contact_id = ? AND channel = ?ORDER BY created_at ASCLIMIT 20Mapped to aisdk::core::Message:
direction = Inbound→Message::Userdirection = Outbound→Message::Assistant- Empty bodies are filtered out
- The current inbound message is appended last
Agentic Loop
Section titled “Agentic Loop”File: src/mods/text_agent/services/generate_text_agent_suggestions_service.rs
The generation runs an agentic loop — not a single completion call. The model can take up to 5 steps before producing structured output.
run_agentic_loop(model, text_agent_id, system_prompt, messages) → stop_when(step_count_is(5)) → schema::<TextAgentSuggestions>() // forced structured JSON output → with_tool(query_knowledge_tool) // optional knowledge base lookupquery_knowledge Tool
Section titled “query_knowledge Tool”| Property | Value |
|---|---|
| Name | "query_knowledge" |
| Input | { query: String } |
| Implementation | handle_query_knowledge_tool_call(agent_id, query) |
| Searches | All knowledge bases linked via text_agent.knowledge_base_ids |
The model calls this tool when it needs factual information before composing suggestions — for example, to look up business hours, pricing, or policy details.
Structured Output
Section titled “Structured Output”The model is required to return a JSON object matching:
#[derive(JsonSchema)]pub struct TextAgentSuggestions { pub suggestions: Vec<TextAgentSuggestionItem>, // always exactly 3}
#[derive(JsonSchema)]pub struct TextAgentSuggestionItem { pub body: String, // the reply text pub confidence: f64, // 0.0 – 1.0}The JSON schema is enforced by the AI SDK’s structured output feature — the model cannot return malformed output.
Provider Routing
Section titled “Provider Routing”agent.provider = OpenAi → generate_openai_suggestions() OpenAI::builder().api_key(core_conf.openai_api_key)
agent.provider = Gemini → generate_gemini_suggestions() Google::builder().api_key(core_conf.google_api_key)Both use the same agentic loop and structured output schema. The only difference is the underlying model builder.
Suggestion Persistence
Section titled “Suggestion Persistence”After generation, suggestions are saved to text_agent_suggestion:
INSERT INTO text_agent_suggestion ( id, organization_id, contact_id, text_agent_id, message_id, suggestions, auto_sent_body, created_at)suggestions is a JSONB array of TextAgentSuggestionItem. auto_sent_body is NULL unless auto-reply fires.
Auto-Reply Logic
Section titled “Auto-Reply Logic”if auto_reply == false: → done (suggestions saved, human picks via messaging view)
if auto_reply == true: best = suggestion with highest confidence if best.confidence >= agent.confidence_threshold: send_auto_reply(best.body, phone_number_id, contact_id, channel) INSERT outbound message record UPDATE text_agent_suggestion.auto_sent_body = best.body else: send_auto_reply(best.body, ...) // still sends INSERT contact_note (warning: low confidence auto-reply)Suggestions in the UI
Section titled “Suggestions in the UI”The messaging view renders an inline suggestions panel below each inbound message with saved suggestions.
File: src/mods/text_agent/components/text_agent_suggestions_panel_component.rs
States:
- Loading — fetches existing suggestions for the message
- No suggestions — shows a “Generate suggestions” button (calls
POST /api/text-agents/generate-suggestions) - Generating — spinner while the agentic loop runs
- Loaded — 3 cards, each showing confidence badge, body, and Use This Reply button
- Error — error message + retry
Confidence badge colors:
≥ 0.8— emerald (high confidence)≥ 0.5— primary blue (medium)< 0.5— muted (low)
Clicking Use This Reply inserts the body into the compose bar via the on_use callback. The human can edit before sending.
On-Demand Suggestion Generation
Section titled “On-Demand Suggestion Generation”Suggestions can also be triggered manually from the messaging view (for messages that arrived before an agent was assigned, or for re-generation):
POST /api/text-agents/generate-suggestions?contact_id={id}&message_id={id}This runs the full pipeline (agent resolution → context → agentic loop → persist) and returns the TextAgentSuggestion record.
GET /api/text-agents/suggestions/{message_id}Returns existing suggestions for a message without re-generating.
Module Structure
Section titled “Module Structure”src/mods/text_agent/services/├── handle_inbound_message_for_text_agent_service.rs # Entry point├── generate_text_agent_suggestions_service.rs # Agentic loop + provider routing└── build_text_agent_context_service.rs # System prompt + conversation history
src/mods/text_agent/api/└── suggestions_api.rs # POST/GET suggestion endpoints
src/mods/text_agent/components/└── text_agent_suggestions_panel_component.rs # Inline suggestions UI
src/mods/twilio/api/└── twilio_events_api.rs # Webhook entry point