Skip to content

Inbound Processing

This page covers the full pipeline from an inbound Twilio message to persisted suggestions in the database.

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.

src/mods/text_agent/services/handle_inbound_message_for_text_agent_service.rs
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 skip

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.

SectionSource
Agent name + purposetext_agent.name + text_agent.purpose
Organization contextbuild_organization_context() — org profile (name, industry, hours, etc.)
Contact namecontact.first_name + contact.last_name
Contact phonesAll rows in contact_phone
Contact tagsJoined from contact_contact_tag + contact_tag
Contact notesAll 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.

SELECT * FROM message
WHERE contact_id = ? AND channel = ?
ORDER BY created_at ASC
LIMIT 20

Mapped to aisdk::core::Message:

  • direction = InboundMessage::User
  • direction = OutboundMessage::Assistant
  • Empty bodies are filtered out
  • The current inbound message is appended last

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 lookup
PropertyValue
Name"query_knowledge"
Input{ query: String }
Implementationhandle_query_knowledge_tool_call(agent_id, query)
SearchesAll 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.

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.

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.

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.

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)

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.

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.

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