This tutorial builds an AI agent that communicates with customers across email, SMS, and voice — choosing the right channel automatically and maintaining shared context across all of them.
The agent uses Claude API for tool-calling (the AI brain), Telnyx APIs for email/SMS/voice delivery (the communication layer), and SQLite for persistent cross-channel context.
I work with Telnyx. The code is available here: https://github.com/team-telnyx/telnyx-code-examples/tree/main/omnichannel-ai-agent-python
What you will build
An AI agent that:
- Receives a customer scenario (e.g., billing dispute)
- Retrieves the full cross-channel conversation history from SQLite
- Uses Claude tool-calling to decide which channel to use next
- Sends the message via Telnyx (Email, SMS, or Voice)
- Stores the interaction and loops until the issue is resolved
Each message references previous interactions across channels — the SMS mentions the email, the voice call mentions both.
Prerequisites
- Python 3.11+
- A Telnyx account with API key, phone number, and verified email sender (or use the demo — no credentials needed)
- An Anthropic API key for Claude (or use the demo)
Project structure
omnichannel-ai-agent-python/
├── app.py # Main Flask app (~300 lines)
├── demo/
│ └── demo_server.py # Full demo without credentials
├── smoke_test.py # 11 smoke tests
├── requirements.txt
├── .env.example
└── README.md
Step 1: Install dependencies
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/omnichannel-ai-agent-python
pip install -r requirements.txt
Dependencies: flask, telnyx, anthropic, python-dotenv, requests.
Step 2: Understand the architecture
Claude API (AI Brain)
│
Tool-calling decides
which channel to use
│
┌────┴────┐
│ Flask │
│ app.py │
│ │
┌─────┤ Context ├─────┐
│ │ (SQLite)│ │
│ └────┬────┘ │
│ │ │
Email API Messaging Call Control
POST /v2/ POST /v2/ POST /v2/
email_messages messages calls
The AI brain (Claude) receives four tool definitions and the conversation history. It decides which tool to call, and the Flask app executes it via the corresponding Telnyx API.
Step 3: Define the tools
Claude uses tool-calling to decide channel routing. Each tool maps to a Telnyx API:
TOOLS = [
{
"name": "send_email",
"description": (
"Send a detailed email to the customer. Use for formal "
"acknowledgments, detailed explanations, or when a written "
"record is needed."
),
"input_schema": {
"type": "object",
"properties": {
"subject": {"type": "string", "description": "Email subject line"},
"body": {"type": "string", "description": "Email body text"},
},
"required": ["subject", "body"],
},
},
{
"name": "send_sms",
"description": (
"Send a short SMS text message to the customer. Use for quick "
"status updates, confirmations, or time-sensitive notifications."
),
"input_schema": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "SMS message text"},
},
"required": ["text"],
},
},
{
"name": "make_call",
"description": (
"Call the customer and speak a message. Use for complex "
"resolution, urgent matters, or when a personal touch is needed."
),
"input_schema": {
"type": "object",
"properties": {
"speak_text": {"type": "string", "description": "Text to speak"},
},
"required": ["speak_text"],
},
},
{
"name": "resolve_issue",
"description": "Mark the customer issue as resolved.",
"input_schema": {
"type": "object",
"properties": {
"summary": {"type": "string", "description": "Resolution summary"},
},
"required": ["summary"],
},
},
]
The descriptions are the routing rules. Claude reads "formal acknowledgments" → email. "Quick status updates" → SMS. "Complex resolution" → voice call.
Step 4: Build the context store
SQLite stores every interaction across all channels:
def init_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id TEXT NOT NULL,
channel TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL
)
""")
cur.execute(
"CREATE INDEX IF NOT EXISTS idx_conv_customer ON conversations (customer_id)"
)
conn.commit()
conn.close()
Before each agent run, the full history is retrieved:
def get_conversation_history(customer_id):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(
"SELECT channel, role, content, timestamp FROM conversations "
"WHERE customer_id = ? ORDER BY timestamp",
(customer_id,),
)
rows = cur.fetchall()
conn.close()
return [dict(r) for r in rows]
Step 5: Implement the channel functions
Each channel function hits a Telnyx API and stores the message:
Email (Telnyx Email API)
def send_email(to_email, subject, body):
resp = requests.post(
"https://api.telnyx.com/v2/email_messages",
headers={
"Authorization": f"Bearer {TELNYX_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": {"email": TELNYX_EMAIL_FROM},
"to": [{"email": to_email}],
"subject": subject,
"body": body,
},
timeout=15,
)
resp.raise_for_status()
return resp.json()
SMS (Telnyx Messaging API)
def send_sms(to_number, text):
payload = {"from": TELNYX_FROM_NUMBER, "to": to_number, "text": text}
if MESSAGING_PROFILE_ID:
payload["messaging_profile_id"] = MESSAGING_PROFILE_ID
resp = requests.post(
"https://api.telnyx.com/v2/messages",
headers={
"Authorization": f"Bearer {TELNYX_API_KEY}",
"Content-Type": "application/json",
},
json=payload,
timeout=10,
)
resp.raise_for_status()
return resp.json()
Voice (Telnyx Call Control API)
def make_call(to_number, speak_text):
resp = requests.post(
"https://api.telnyx.com/v2/calls",
headers={
"Authorization": f"Bearer {TELNYX_API_KEY}",
"Content-Type": "application/json",
},
json={
"connection_id": CONNECTION_ID,
"to": to_number,
"from": TELNYX_FROM_NUMBER,
},
timeout=10,
)
resp.raise_for_status()
return resp.json()
All three channels use the same API key and base URL. One platform, one authentication.
Step 6: Build the agentic loop
The agent runs a standard Claude tool-calling loop:
def run_agent(customer, scenario):
history = get_conversation_history(customer["id"])
messages = [{"role": "user", "content": f"Customer: {customer['name']}...\nIssue: {scenario}"}]
actions = []
while True:
response = claude_client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
break
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool in tool_use_blocks:
result = execute_tool(tool.name, tool.input, customer)
actions.append({"type": "tool_call", "tool": tool.name, "result": result})
tool_results.append({
"type": "tool_result",
"tool_use_id": tool.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
return actions
Step 7: Run the demo
No credentials needed:
python demo/demo_server.py
The demo:
- Starts the Flask server on
localhost:5555 - Automatically triggers the agent with a billing dispute scenario
- Prints each channel firing in sequence: email → SMS → voice
- Stores everything in SQLite
Check the conversation history:
curl http://localhost:5555/conversations | python -m json.tool
Step 8: Run with real credentials
cp .env.example .env
# Edit .env with your Telnyx and Claude API keys
python app.py
Trigger the agent:
curl -X POST http://localhost:5000/agent/run \
-H "Content-Type: application/json" \
-d '{
"customer": {
"id": "cust_001",
"name": "Sarah Chen",
"email": "sarah@example.com",
"phone": "+15551234567"
},
"scenario": "Customer is disputing a $147.50 charge."
}'
Step 9: Run the tests
python -m pytest smoke_test.py -v
11 tests covering module imports, route registration, database operations, tool definitions, and API endpoints. All pass.
What makes this omnichannel
The difference between multichannel and omnichannel is shared context. This agent:
- Stores every interaction in one SQLite table regardless of channel
- Passes the full cross-channel history to Claude before every decision
- References previous channel interactions in each new message
- Uses AI judgment (not hardcoded rules) for channel routing
The SMS mentions the email. The voice call mentions both. One conversation. Three channels.
Resources
- Complete code
- Telnyx Email API
- Telnyx Messaging API
- Telnyx Call Control API
- Claude API Tool Use
- Telnyx Developer Docs
- Create a Telnyx account
Top comments (0)