Skip to content

Commit 14016fd

Browse files
authored
feat: #439 add SIP support for realtime agent runner (#624)
1 parent 1a5326f commit 14016fd

File tree

16 files changed

+704
-7
lines changed

16 files changed

+704
-7
lines changed

.changeset/seven-llamas-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-realtime': patch
3+
---
4+
5+
feat: #439 add SIP support for realtime agent runner
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Twilio SIP Realtime Example
2+
3+
This example shows how to handle OpenAI Realtime SIP calls with the Agents JS SDK. Incoming calls are accepted through the Realtime Calls API, a triage agent answers with a fixed greeting, and handoffs route the caller to specialist agents (FAQ lookup and record updates) similar to the realtime UI demo.
4+
5+
## Prerequisites
6+
7+
- Node.js 22+
8+
- pnpm 10+
9+
- An OpenAI API key with Realtime API access
10+
- A configured webhook secret for your OpenAI project
11+
- A Twilio account with a phone number and Elastic SIP Trunking enabled
12+
- A public HTTPS endpoint for local development (for example, [ngrok](https://ngrok.com/))
13+
14+
## Configure OpenAI
15+
16+
1. In [platform settings](https://platform.openai.com/settings) select your project.
17+
2. Create a webhook pointing to `https://<your-public-host>/openai/webhook` with the **realtime.call.incoming** event type and note the signing secret. The server verifies each webhook using `OPENAI_WEBHOOK_SECRET`.
18+
19+
## Configure Twilio Elastic SIP Trunking
20+
21+
1. Create (or edit) an Elastic SIP trunk.
22+
2. On the **Origination** tab, add an origination SIP URI of `sip:proj_<your_project_id>@sip.api.openai.com;transport=tls` so Twilio sends inbound calls to OpenAI. (The Termination tab always ends with `.pstn.twilio.com`, so leave it unchanged.)
23+
3. Attach at least one phone number to the trunk so inbound calls are forwarded to OpenAI.
24+
25+
## Setup
26+
27+
1. Install dependencies from the monorepo root (if you have not already):
28+
```bash
29+
pnpm install
30+
```
31+
2. Export the required environment variables:
32+
```bash
33+
export OPENAI_API_KEY="sk-..."
34+
export OPENAI_WEBHOOK_SECRET="whsec_..."
35+
export PORT=8000 # optional, defaults to 8000
36+
```
37+
3. (Optional) Adjust the multi-agent logic in `examples/realtime-twilio-sip/agents.ts` if you want to change the specialist agents or tools.
38+
4. Start the Fastify server:
39+
```bash
40+
pnpm -F realtime-twilio-sip start
41+
```
42+
5. Expose the server publicly (example with ngrok):
43+
```bash
44+
ngrok http 8000
45+
```
46+
47+
## Test a Call
48+
49+
1. Place a call to the Twilio number attached to the SIP trunk.
50+
2. Twilio sends the call to `sip.api.openai.com`; OpenAI emits a `realtime.call.incoming` event, which this server accepts via the Realtime Calls API.
51+
3. The triage agent greets the caller, then either keeps the conversation or hands off to:
52+
- **FAQ Agent** – answers common questions via `faq_lookup_tool`.
53+
- **Records Agent** – writes short notes using `update_customer_record`.
54+
4. The background task attaches to the call and logs transcripts plus basic events in the console.
55+
56+
Tweak `server.ts` to customize instructions, add tools, or integrate with internal systems after the SIP session is active.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { RECOMMENDED_PROMPT_PREFIX } from '@openai/agents-core/extensions';
2+
import { RealtimeAgent, tool } from '@openai/agents/realtime';
3+
import { z } from 'zod';
4+
5+
export const WELCOME_MESSAGE =
6+
'Hello, this is ABC customer service. How can I help you today?';
7+
8+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
9+
10+
const faqLookupSchema = z.object({
11+
question: z.string().describe('The caller question to search for.'),
12+
});
13+
14+
const faqLookupTool = tool({
15+
name: 'faq_lookup_tool',
16+
description: 'Lookup frequently asked questions for the caller.',
17+
parameters: faqLookupSchema,
18+
execute: async ({ question }: z.infer<typeof faqLookupSchema>) => {
19+
await wait(1000);
20+
21+
const normalized = question.toLowerCase();
22+
if (normalized.includes('wi-fi') || normalized.includes('wifi')) {
23+
return 'We provide complimentary Wi-Fi. Join the ABC-Customer network.';
24+
}
25+
if (normalized.includes('billing') || normalized.includes('invoice')) {
26+
return 'Your latest invoice is available in the ABC portal under Billing > History.';
27+
}
28+
if (normalized.includes('hours') || normalized.includes('support')) {
29+
return 'Human support agents are available 24/7; transfer to the specialist if needed.';
30+
}
31+
return "I'm not sure about that. Let me transfer you back to the triage agent.";
32+
},
33+
});
34+
35+
const updateCustomerRecordSchema = z.object({
36+
customerId: z
37+
.string()
38+
.describe('Unique identifier for the customer you are updating.'),
39+
note: z
40+
.string()
41+
.describe('Brief summary of the customer request to store in records.'),
42+
});
43+
44+
const updateCustomerRecord = tool({
45+
name: 'update_customer_record',
46+
description: 'Record a short note about the caller.',
47+
parameters: updateCustomerRecordSchema,
48+
execute: async ({
49+
customerId,
50+
note,
51+
}: z.infer<typeof updateCustomerRecordSchema>) => {
52+
await wait(1000);
53+
return `Recorded note for ${customerId}: ${note}`;
54+
},
55+
});
56+
57+
const faqAgent = new RealtimeAgent({
58+
name: 'FAQ Agent',
59+
handoffDescription:
60+
'Handles frequently asked questions and general account inquiries.',
61+
instructions: `${RECOMMENDED_PROMPT_PREFIX}
62+
You are an FAQ specialist. Always rely on the faq_lookup_tool for answers and keep replies concise. If the caller needs hands-on help, transfer back to the triage agent.`,
63+
tools: [faqLookupTool],
64+
});
65+
66+
const recordsAgent = new RealtimeAgent({
67+
name: 'Records Agent',
68+
handoffDescription:
69+
'Updates customer records with brief notes and confirmation numbers.',
70+
instructions: `${RECOMMENDED_PROMPT_PREFIX}
71+
You handle structured updates. Confirm the customer's ID, capture their request in a short note, and use the update_customer_record tool. For anything outside data updates, return to the triage agent.`,
72+
tools: [updateCustomerRecord],
73+
});
74+
75+
const triageAgent = new RealtimeAgent({
76+
name: 'Triage Agent',
77+
handoffDescription:
78+
'Greets callers and routes them to the most appropriate specialist.',
79+
instructions: `${RECOMMENDED_PROMPT_PREFIX}
80+
Always begin the call by saying exactly '${WELCOME_MESSAGE}' before collecting details. Once the greeting is complete, gather context and hand off to the FAQ or Records agents when appropriate.`,
81+
handoffs: [faqAgent, recordsAgent],
82+
});
83+
84+
faqAgent.handoffs = [triageAgent, recordsAgent];
85+
recordsAgent.handoffs = [triageAgent, faqAgent];
86+
87+
export function getStartingAgent(): RealtimeAgent {
88+
return triageAgent;
89+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"private": true,
3+
"name": "realtime-twilio-sip",
4+
"dependencies": {
5+
"@openai/agents-core": "workspace:*",
6+
"@openai/agents-realtime": "workspace:*",
7+
"@openai/agents": "workspace:*",
8+
"dotenv": "^16.5.0",
9+
"fastify": "^5.3.3",
10+
"fastify-raw-body": "^5.0.0",
11+
"openai": "^6.7.0"
12+
},
13+
"scripts": {
14+
"build-check": "tsc --noEmit",
15+
"start": "tsx server.ts"
16+
}
17+
}

0 commit comments

Comments
 (0)