Skip to content

Commit 52ad62a

Browse files
authored
Merge branch 'main' into log-tool-errors
2 parents a6a884a + 0c4f2b9 commit 52ad62a

File tree

20 files changed

+1249
-177
lines changed

20 files changed

+1249
-177
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ jobs:
5151
- "3.11"
5252
- "3.12"
5353
- "3.13"
54-
# TODO: enable this https://github.com/openai/openai-agents-python/pull/1961/
55-
# - "3.14"
54+
- "3.14"
5655
env:
5756
OPENAI_API_KEY: fake-for-tests
5857
steps:

examples/basic/lifecycle_example.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import random
3-
from typing import Any, Optional
3+
from typing import Any, Optional, cast
44

55
from pydantic import BaseModel
66

@@ -15,6 +15,7 @@
1515
function_tool,
1616
)
1717
from agents.items import ModelResponse, TResponseInputItem
18+
from agents.tool_context import ToolContext
1819

1920

2021
class LoggingHooks(AgentHooks[Any]):
@@ -71,16 +72,22 @@ async def on_agent_end(self, context: RunContextWrapper, agent: Agent, output: A
7172

7273
async def on_tool_start(self, context: RunContextWrapper, agent: Agent, tool: Tool) -> None:
7374
self.event_counter += 1
75+
# While this type cast is not ideal,
76+
# we don't plan to change the context arg type in the near future for backwards compatibility.
77+
tool_context = cast(ToolContext[Any], context)
7478
print(
75-
f"### {self.event_counter}: Tool {tool.name} started. name={context.tool_name}, call_id={context.tool_call_id}, args={context.tool_arguments}. Usage: {self._usage_to_str(context.usage)}" # type: ignore[attr-defined]
79+
f"### {self.event_counter}: Tool {tool.name} started. name={tool_context.tool_name}, call_id={tool_context.tool_call_id}, args={tool_context.tool_arguments}. Usage: {self._usage_to_str(tool_context.usage)}"
7680
)
7781

7882
async def on_tool_end(
7983
self, context: RunContextWrapper, agent: Agent, tool: Tool, result: str
8084
) -> None:
8185
self.event_counter += 1
86+
# While this type cast is not ideal,
87+
# we don't plan to change the context arg type in the near future for backwards compatibility.
88+
tool_context = cast(ToolContext[Any], context)
8289
print(
83-
f"### {self.event_counter}: Tool {tool.name} finished. result={result}, name={context.tool_name}, call_id={context.tool_call_id}, args={context.tool_arguments}. Usage: {self._usage_to_str(context.usage)}" # type: ignore[attr-defined]
90+
f"### {self.event_counter}: Tool {tool.name} finished. result={result}, name={tool_context.tool_name}, call_id={tool_context.tool_call_id}, args={tool_context.tool_arguments}. Usage: {self._usage_to_str(tool_context.usage)}"
8491
)
8592

8693
async def on_handoff(
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Twilio SIP Realtime Example
2+
3+
This example shows how to handle OpenAI Realtime SIP calls with the Agents 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+
- Python 3.9+
8+
- An OpenAI API key with Realtime API access
9+
- A configured webhook secret for your OpenAI project
10+
- A Twilio account with a phone number and Elastic SIP Trunking enabled
11+
- A public HTTPS endpoint for local development (for example, [ngrok](https://ngrok.com/))
12+
13+
## Configure OpenAI
14+
15+
1. In [platform settings](https://platform.openai.com/settings) select your project.
16+
2. Create a webhook pointing to `https://<your-public-host>/openai/webhook` with "realtime.call.incoming" event type and note the signing secret. The example verifies each webhook with `OPENAI_WEBHOOK_SECRET`.
17+
18+
## Configure Twilio Elastic SIP Trunking
19+
20+
1. Create (or edit) an Elastic SIP trunk.
21+
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.)
22+
3. Add at least one phone number to the trunk so inbound calls are forwarded to OpenAI.
23+
24+
## Setup
25+
26+
1. Install dependencies:
27+
```bash
28+
uv pip install -r examples/realtime/twilio-sip/requirements.txt
29+
```
30+
2. Export required environment variables:
31+
```bash
32+
export OPENAI_API_KEY="sk-..."
33+
export OPENAI_WEBHOOK_SECRET="whsec_..."
34+
```
35+
3. (Optional) Adjust the multi-agent logic in `examples/realtime/twilio_sip/agents.py` if you want
36+
to change the specialist agents or tools.
37+
4. Run the FastAPI server:
38+
```bash
39+
uv run uvicorn examples.realtime.twilio_sip.server:app --host 0.0.0.0 --port 8000
40+
```
41+
5. Expose the server publicly (example with ngrok):
42+
```bash
43+
ngrok http 8000
44+
```
45+
46+
## Test a Call
47+
48+
1. Place a call to the Twilio number attached to the SIP trunk.
49+
2. Twilio sends the call to `sip.api.openai.com`; OpenAI fires `realtime.call.incoming`, which this example accepts.
50+
3. The triage agent greets the caller, then either keeps the conversation or hands off to:
51+
- **FAQ Agent** – answers common questions via `faq_lookup_tool`.
52+
- **Records Agent** – writes short notes using `update_customer_record`.
53+
4. The background task attaches to the call and logs transcripts plus basic events in the console.
54+
55+
You can edit `server.py` to change instructions, add tools, or integrate with internal systems once the SIP session is active.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""OpenAI Realtime SIP example package."""
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Realtime agent definitions shared by the Twilio SIP example."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
7+
from agents import function_tool
8+
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
9+
from agents.realtime import RealtimeAgent, realtime_handoff
10+
11+
# --- Tools -----------------------------------------------------------------
12+
13+
14+
WELCOME_MESSAGE = "Hello, this is ABC customer service. How can I help you today?"
15+
16+
17+
@function_tool(
18+
name_override="faq_lookup_tool", description_override="Lookup frequently asked questions."
19+
)
20+
async def faq_lookup_tool(question: str) -> str:
21+
"""Fetch FAQ answers for the caller."""
22+
23+
await asyncio.sleep(3)
24+
25+
q = question.lower()
26+
if "plan" in q or "wifi" in q or "wi-fi" in q:
27+
return "We provide complimentary Wi-Fi. Join the ABC-Customer network." # demo data
28+
if "billing" in q or "invoice" in q:
29+
return "Your latest invoice is available in the ABC portal under Billing > History."
30+
if "hours" in q or "support" in q:
31+
return "Human support agents are available 24/7; transfer to the specialist if needed."
32+
return "I'm not sure about that. Let me transfer you back to the triage agent."
33+
34+
35+
@function_tool
36+
async def update_customer_record(customer_id: str, note: str) -> str:
37+
"""Record a short note about the caller."""
38+
39+
await asyncio.sleep(1)
40+
return f"Recorded note for {customer_id}: {note}"
41+
42+
43+
# --- Agents ----------------------------------------------------------------
44+
45+
46+
faq_agent = RealtimeAgent(
47+
name="FAQ Agent",
48+
handoff_description="Handles frequently asked questions and general account inquiries.",
49+
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
50+
You are an FAQ specialist. Always rely on the faq_lookup_tool for answers and keep replies
51+
concise. If the caller needs hands-on help, transfer back to the triage agent.
52+
""",
53+
tools=[faq_lookup_tool],
54+
)
55+
56+
records_agent = RealtimeAgent(
57+
name="Records Agent",
58+
handoff_description="Updates customer records with brief notes and confirmation numbers.",
59+
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
60+
You handle structured updates. Confirm the customer's ID, capture their request in a short
61+
note, and use the update_customer_record tool. For anything outside data updates, return to the
62+
triage agent.
63+
""",
64+
tools=[update_customer_record],
65+
)
66+
67+
triage_agent = RealtimeAgent(
68+
name="Triage Agent",
69+
handoff_description="Greets callers and routes them to the most appropriate specialist.",
70+
instructions=(
71+
f"{RECOMMENDED_PROMPT_PREFIX} "
72+
"Always begin the call by saying exactly: '"
73+
f"{WELCOME_MESSAGE}' "
74+
"before collecting details. Once the greeting is complete, gather context and hand off to "
75+
"the FAQ or Records agents when appropriate."
76+
),
77+
handoffs=[faq_agent, realtime_handoff(records_agent)],
78+
)
79+
80+
faq_agent.handoffs.append(triage_agent)
81+
records_agent.handoffs.append(triage_agent)
82+
83+
84+
def get_starting_agent() -> RealtimeAgent:
85+
"""Return the agent used to start each realtime call."""
86+
87+
return triage_agent
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fastapi>=0.120.0
2+
openai>=2.2,<3
3+
uvicorn[standard]>=0.38.0

0 commit comments

Comments
 (0)