Skip to content

Commit 6293d66

Browse files
authored
Realtime: introduce a demo without a UI (#1135)
The UI demo is nice, but this let you print more logs to the console (at the cost of being ugly)
1 parent c7d50cb commit 6293d66

File tree

2 files changed

+180
-1
lines changed

2 files changed

+180
-1
lines changed

examples/realtime/demo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ async def _on_event(self, event: RealtimeSessionEvent) -> None:
103103
elif event.type == "history_added":
104104
pass
105105
elif event.type == "raw_model_event":
106-
self.ui.log_message(f"Raw model event: {_truncate_str(str(event.data), 50)}")
106+
if event.data.type != "error" and event.data.type != "exception":
107+
self.ui.log_message(f"Raw model event: {event.data}")
107108
else:
108109
self.ui.log_message(f"Unknown event type: {event.type}")
109110
except Exception as e:

examples/realtime/no_ui_demo.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import asyncio
2+
import sys
3+
4+
import numpy as np
5+
import sounddevice as sd
6+
7+
from agents import function_tool
8+
from agents.realtime import RealtimeAgent, RealtimeRunner, RealtimeSession, RealtimeSessionEvent
9+
10+
# Audio configuration
11+
CHUNK_LENGTH_S = 0.05 # 50ms
12+
SAMPLE_RATE = 24000
13+
FORMAT = np.int16
14+
CHANNELS = 1
15+
16+
# Set up logging for OpenAI agents SDK
17+
# logging.basicConfig(
18+
# level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
19+
# )
20+
# logger.logger.setLevel(logging.ERROR)
21+
22+
23+
@function_tool
24+
def get_weather(city: str) -> str:
25+
"""Get the weather in a city."""
26+
return f"The weather in {city} is sunny."
27+
28+
29+
agent = RealtimeAgent(
30+
name="Assistant",
31+
instructions="You always greet the user with 'Top of the morning to you'.",
32+
tools=[get_weather],
33+
)
34+
35+
36+
def _truncate_str(s: str, max_length: int) -> str:
37+
if len(s) > max_length:
38+
return s[:max_length] + "..."
39+
return s
40+
41+
42+
class NoUIDemo:
43+
def __init__(self) -> None:
44+
self.session: RealtimeSession | None = None
45+
self.audio_stream: sd.InputStream | None = None
46+
self.audio_player: sd.OutputStream | None = None
47+
self.recording = False
48+
49+
async def run(self) -> None:
50+
print("Connecting, may take a few seconds...")
51+
52+
# Initialize audio player
53+
self.audio_player = sd.OutputStream(
54+
channels=CHANNELS,
55+
samplerate=SAMPLE_RATE,
56+
dtype=FORMAT,
57+
)
58+
self.audio_player.start()
59+
60+
try:
61+
runner = RealtimeRunner(agent)
62+
async with await runner.run() as session:
63+
self.session = session
64+
print("Connected. Starting audio recording...")
65+
66+
# Start audio recording
67+
await self.start_audio_recording()
68+
print("Audio recording started. You can start speaking - expect lots of logs!")
69+
70+
# Process session events
71+
async for event in session:
72+
await self._on_event(event)
73+
74+
finally:
75+
# Clean up audio player
76+
if self.audio_player and self.audio_player.active:
77+
self.audio_player.stop()
78+
if self.audio_player:
79+
self.audio_player.close()
80+
81+
print("Session ended")
82+
83+
async def start_audio_recording(self) -> None:
84+
"""Start recording audio from the microphone."""
85+
# Set up audio input stream
86+
self.audio_stream = sd.InputStream(
87+
channels=CHANNELS,
88+
samplerate=SAMPLE_RATE,
89+
dtype=FORMAT,
90+
)
91+
92+
self.audio_stream.start()
93+
self.recording = True
94+
95+
# Start audio capture task
96+
asyncio.create_task(self.capture_audio())
97+
98+
async def capture_audio(self) -> None:
99+
"""Capture audio from the microphone and send to the session."""
100+
if not self.audio_stream or not self.session:
101+
return
102+
103+
# Buffer size in samples
104+
read_size = int(SAMPLE_RATE * CHUNK_LENGTH_S)
105+
106+
try:
107+
while self.recording:
108+
# Check if there's enough data to read
109+
if self.audio_stream.read_available < read_size:
110+
await asyncio.sleep(0.01)
111+
continue
112+
113+
# Read audio data
114+
data, _ = self.audio_stream.read(read_size)
115+
116+
# Convert numpy array to bytes
117+
audio_bytes = data.tobytes()
118+
119+
# Send audio to session
120+
await self.session.send_audio(audio_bytes)
121+
122+
# Yield control back to event loop
123+
await asyncio.sleep(0)
124+
125+
except Exception as e:
126+
print(f"Audio capture error: {e}")
127+
finally:
128+
if self.audio_stream and self.audio_stream.active:
129+
self.audio_stream.stop()
130+
if self.audio_stream:
131+
self.audio_stream.close()
132+
133+
async def _on_event(self, event: RealtimeSessionEvent) -> None:
134+
"""Handle session events."""
135+
try:
136+
if event.type == "agent_start":
137+
print(f"Agent started: {event.agent.name}")
138+
elif event.type == "agent_end":
139+
print(f"Agent ended: {event.agent.name}")
140+
elif event.type == "handoff":
141+
print(f"Handoff from {event.from_agent.name} to {event.to_agent.name}")
142+
elif event.type == "tool_start":
143+
print(f"Tool started: {event.tool.name}")
144+
elif event.type == "tool_end":
145+
print(f"Tool ended: {event.tool.name}; output: {event.output}")
146+
elif event.type == "audio_end":
147+
print("Audio ended")
148+
elif event.type == "audio":
149+
# Play audio through speakers
150+
np_audio = np.frombuffer(event.audio.data, dtype=np.int16)
151+
if self.audio_player:
152+
try:
153+
self.audio_player.write(np_audio)
154+
except Exception as e:
155+
print(f"Audio playback error: {e}")
156+
elif event.type == "audio_interrupted":
157+
print("Audio interrupted")
158+
elif event.type == "error":
159+
print(f"Error: {event.error}")
160+
elif event.type == "history_updated":
161+
pass # Skip these frequent events
162+
elif event.type == "history_added":
163+
pass # Skip these frequent events
164+
elif event.type == "raw_model_event":
165+
print(f"Raw model event: {_truncate_str(str(event.data), 50)}")
166+
else:
167+
print(f"Unknown event type: {event.type}")
168+
except Exception as e:
169+
print(f"Error processing event: {_truncate_str(str(e), 50)}")
170+
171+
172+
if __name__ == "__main__":
173+
demo = NoUIDemo()
174+
try:
175+
asyncio.run(demo.run())
176+
except KeyboardInterrupt:
177+
print("\nExiting...")
178+
sys.exit(0)

0 commit comments

Comments
 (0)