Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 87a7a87

Browse files
authored
Merge branch 'main' into feature/function-call-args-streaming
2 parents c5d982e + 8fdbe09 commit 87a7a87

File tree

9 files changed

+160
-61
lines changed

9 files changed

+160
-61
lines changed

docs/release.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ We will increment `Z` for non-breaking changes:
1919

2020
## Breaking change changelog
2121

22+
### 0.2.0
23+
24+
In this version, a few places that used to take `Agent` as an arg, now take `AgentBase` as an arg instead. For example, the `list_tools()` call in MCP servers. This is a purely typing change, you will still receive `Agent` objects. To update, just fix type errors by replacing `Agent` with `AgentBase`.
25+
2226
### 0.1.0
2327

2428
In this version, [`MCPServer.list_tools()`][agents.mcp.server.MCPServer] has two new params: `run_context` and `agent`. You'll need to add these params to any classes that subclass `MCPServer`.

examples/realtime/demo.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ def get_weather(city: str) -> str:
3838
)
3939

4040

41+
def _truncate_str(s: str, max_length: int) -> str:
42+
if len(s) > max_length:
43+
return s[:max_length] + "..."
44+
return s
45+
46+
4147
class Example:
4248
def __init__(self) -> None:
4349
self.ui = AppUI()
@@ -70,33 +76,38 @@ async def on_audio_recorded(self, audio_bytes: bytes) -> None:
7076
await self.session.send_audio(audio_bytes)
7177

7278
async def _on_event(self, event: RealtimeSessionEvent) -> None:
73-
if event.type == "agent_start":
74-
self.ui.add_transcript(f"Agent started: {event.agent.name}")
75-
elif event.type == "agent_end":
76-
self.ui.add_transcript(f"Agent ended: {event.agent.name}")
77-
elif event.type == "handoff":
78-
self.ui.add_transcript(f"Handoff from {event.from_agent.name} to {event.to_agent.name}")
79-
elif event.type == "tool_start":
80-
self.ui.add_transcript(f"Tool started: {event.tool.name}")
81-
elif event.type == "tool_end":
82-
self.ui.add_transcript(f"Tool ended: {event.tool.name}; output: {event.output}")
83-
elif event.type == "audio_end":
84-
self.ui.add_transcript("Audio ended")
85-
elif event.type == "audio":
86-
np_audio = np.frombuffer(event.audio.data, dtype=np.int16)
87-
self.ui.play_audio(np_audio)
88-
elif event.type == "audio_interrupted":
89-
self.ui.add_transcript("Audio interrupted")
90-
elif event.type == "error":
91-
self.ui.add_transcript(f"Error: {event.error}")
92-
elif event.type == "history_updated":
93-
pass
94-
elif event.type == "history_added":
95-
pass
96-
elif event.type == "raw_model_event":
97-
self.ui.log_message(f"Raw model event: {event.data}")
98-
else:
99-
self.ui.log_message(f"Unknown event type: {event.type}")
79+
try:
80+
if event.type == "agent_start":
81+
self.ui.add_transcript(f"Agent started: {event.agent.name}")
82+
elif event.type == "agent_end":
83+
self.ui.add_transcript(f"Agent ended: {event.agent.name}")
84+
elif event.type == "handoff":
85+
self.ui.add_transcript(
86+
f"Handoff from {event.from_agent.name} to {event.to_agent.name}"
87+
)
88+
elif event.type == "tool_start":
89+
self.ui.add_transcript(f"Tool started: {event.tool.name}")
90+
elif event.type == "tool_end":
91+
self.ui.add_transcript(f"Tool ended: {event.tool.name}; output: {event.output}")
92+
elif event.type == "audio_end":
93+
self.ui.add_transcript("Audio ended")
94+
elif event.type == "audio":
95+
np_audio = np.frombuffer(event.audio.data, dtype=np.int16)
96+
self.ui.play_audio(np_audio)
97+
elif event.type == "audio_interrupted":
98+
self.ui.add_transcript("Audio interrupted")
99+
elif event.type == "error":
100+
pass
101+
elif event.type == "history_updated":
102+
pass
103+
elif event.type == "history_added":
104+
pass
105+
elif event.type == "raw_model_event":
106+
self.ui.log_message(f"Raw model event: {_truncate_str(str(event.data), 50)}")
107+
else:
108+
self.ui.log_message(f"Unknown event type: {event.type}")
109+
except Exception as e:
110+
self.ui.log_message(f"Error processing event: {_truncate_str(str(e), 50)}")
100111

101112

102113
if __name__ == "__main__":

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "openai-agents"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = "OpenAI Agents SDK"
55
readme = "README.md"
66
requires-python = ">=3.9"

src/agents/realtime/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
RealtimeToolStart,
3232
)
3333
from .items import (
34-
AssistantAudio,
3534
AssistantMessageItem,
3635
AssistantText,
3736
InputAudio,
@@ -123,7 +122,6 @@
123122
"RealtimeToolEnd",
124123
"RealtimeToolStart",
125124
# Items
126-
"AssistantAudio",
127125
"AssistantMessageItem",
128126
"AssistantText",
129127
"InputAudio",

src/agents/realtime/items.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class InputText(BaseModel):
99
type: Literal["input_text"] = "input_text"
10-
text: str
10+
text: str | None = None
1111

1212
# Allow extra data
1313
model_config = ConfigDict(extra="allow")
@@ -24,16 +24,7 @@ class InputAudio(BaseModel):
2424

2525
class AssistantText(BaseModel):
2626
type: Literal["text"] = "text"
27-
text: str
28-
29-
# Allow extra data
30-
model_config = ConfigDict(extra="allow")
31-
32-
33-
class AssistantAudio(BaseModel):
34-
type: Literal["audio"] = "audio"
35-
audio: str | None = None
36-
transcript: str | None = None
27+
text: str | None = None
3728

3829
# Allow extra data
3930
model_config = ConfigDict(extra="allow")
@@ -55,7 +46,7 @@ class UserMessageItem(BaseModel):
5546
previous_item_id: str | None = None
5647
type: Literal["message"] = "message"
5748
role: Literal["user"] = "user"
58-
content: list[InputText | InputAudio]
49+
content: list[Annotated[InputText | InputAudio, Field(discriminator="type")]]
5950

6051
# Allow extra data
6152
model_config = ConfigDict(extra="allow")
@@ -67,7 +58,7 @@ class AssistantMessageItem(BaseModel):
6758
type: Literal["message"] = "message"
6859
role: Literal["assistant"] = "assistant"
6960
status: Literal["in_progress", "completed", "incomplete"] | None = None
70-
content: list[AssistantText | AssistantAudio]
61+
content: list[AssistantText]
7162

7263
# Allow extra data
7364
model_config = ConfigDict(extra="allow")

src/agents/realtime/openai_realtime.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -388,15 +388,8 @@ async def _handle_conversation_item(
388388
self, item: ConversationItem, previous_item_id: str | None
389389
) -> None:
390390
"""Handle conversation item creation/retrieval events."""
391-
message_item: RealtimeMessageItem = TypeAdapter(RealtimeMessageItem).validate_python(
392-
{
393-
"item_id": item.id or "",
394-
"previous_item_id": previous_item_id,
395-
"type": item.type,
396-
"role": item.role,
397-
"content": item.content,
398-
"status": "in_progress",
399-
}
391+
message_item = _ConversionHelper.conversation_item_to_realtime_message_item(
392+
item, previous_item_id
400393
)
401394
await self._emit_event(RealtimeModelItemUpdatedEvent(item=message_item))
402395

@@ -418,6 +411,8 @@ async def _cancel_response(self) -> None:
418411

419412
async def _handle_ws_event(self, event: dict[str, Any]):
420413
try:
414+
if "previous_item_id" in event and event["previous_item_id"] is None:
415+
event["previous_item_id"] = "" # TODO (rm) remove
421416
parsed: OpenAIRealtimeServerEvent = TypeAdapter(
422417
OpenAIRealtimeServerEvent
423418
).validate_python(event)
@@ -465,7 +460,8 @@ async def _handle_ws_event(self, event: dict[str, Any]):
465460
previous_item_id = (
466461
parsed.previous_item_id if parsed.type == "conversation.item.created" else None
467462
)
468-
await self._handle_conversation_item(parsed.item, previous_item_id)
463+
if parsed.item.type == "message":
464+
await self._handle_conversation_item(parsed.item, previous_item_id)
469465
elif (
470466
parsed.type == "conversation.item.input_audio_transcription.completed"
471467
or parsed.type == "conversation.item.truncated"
@@ -567,3 +563,22 @@ def _tools_to_session_tools(self, tools: list[Tool]) -> list[OpenAISessionTool]:
567563
)
568564
)
569565
return converted_tools
566+
567+
568+
class _ConversionHelper:
569+
@classmethod
570+
def conversation_item_to_realtime_message_item(
571+
cls, item: ConversationItem, previous_item_id: str | None
572+
) -> RealtimeMessageItem:
573+
return TypeAdapter(RealtimeMessageItem).validate_python(
574+
{
575+
"item_id": item.id or "",
576+
"previous_item_id": previous_item_id,
577+
"type": item.type,
578+
"role": item.role,
579+
"content": (
580+
[content.model_dump() for content in item.content] if item.content else []
581+
),
582+
"status": "in_progress",
583+
},
584+
)

tests/realtime/test_item_parsing.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from openai.types.beta.realtime.conversation_item import ConversationItem
2+
from openai.types.beta.realtime.conversation_item_content import ConversationItemContent
3+
4+
from agents.realtime.items import (
5+
AssistantMessageItem,
6+
RealtimeMessageItem,
7+
SystemMessageItem,
8+
UserMessageItem,
9+
)
10+
from agents.realtime.openai_realtime import _ConversionHelper
11+
12+
13+
def test_user_message_conversion() -> None:
14+
item = ConversationItem(
15+
id="123",
16+
type="message",
17+
role="user",
18+
content=[
19+
ConversationItemContent(
20+
id=None, audio=None, text=None, transcript=None, type="input_text"
21+
)
22+
],
23+
)
24+
25+
converted: RealtimeMessageItem = _ConversionHelper.conversation_item_to_realtime_message_item(
26+
item, None
27+
)
28+
29+
assert isinstance(converted, UserMessageItem)
30+
31+
item = ConversationItem(
32+
id="123",
33+
type="message",
34+
role="user",
35+
content=[
36+
ConversationItemContent(
37+
id=None, audio=None, text=None, transcript=None, type="input_audio"
38+
)
39+
],
40+
)
41+
42+
converted = _ConversionHelper.conversation_item_to_realtime_message_item(item, None)
43+
44+
assert isinstance(converted, UserMessageItem)
45+
46+
47+
def test_assistant_message_conversion() -> None:
48+
item = ConversationItem(
49+
id="123",
50+
type="message",
51+
role="assistant",
52+
content=[
53+
ConversationItemContent(id=None, audio=None, text=None, transcript=None, type="text")
54+
],
55+
)
56+
57+
converted: RealtimeMessageItem = _ConversionHelper.conversation_item_to_realtime_message_item(
58+
item, None
59+
)
60+
61+
assert isinstance(converted, AssistantMessageItem)
62+
63+
64+
def test_system_message_conversion() -> None:
65+
item = ConversationItem(
66+
id="123",
67+
type="message",
68+
role="system",
69+
content=[
70+
ConversationItemContent(
71+
id=None, audio=None, text=None, transcript=None, type="input_text"
72+
)
73+
],
74+
)
75+
76+
converted: RealtimeMessageItem = _ConversionHelper.conversation_item_to_realtime_message_item(
77+
item, None
78+
)
79+
80+
assert isinstance(converted, SystemMessageItem)

tests/realtime/test_session.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ async def test_item_updated_event_updates_existing_item(self, mock_model, mock_a
293293
# Check that item was updated
294294
assert len(session._history) == 1
295295
updated_item = cast(AssistantMessageItem, session._history[0])
296-
assert cast(AssistantText, updated_item.content[0]).text == "Updated"
296+
assert updated_item.content[0].text == "Updated"
297297

298298
# Should have 2 events: raw + history updated (not added)
299299
assert session._event_queue.qsize() == 2
@@ -524,7 +524,7 @@ def test_update_existing_item_by_id(self):
524524
# Item should be updated
525525
result_item = cast(AssistantMessageItem, new_history[0])
526526
assert result_item.item_id == "item_1"
527-
assert cast(AssistantText, result_item.content[0]).text == "Updated"
527+
assert result_item.content[0].text == "Updated"
528528

529529
def test_update_existing_item_preserves_order(self):
530530
"""Test that updating existing item preserves its position in history"""
@@ -557,13 +557,13 @@ def test_update_existing_item_preserves_order(self):
557557

558558
# Middle item should be updated
559559
updated_result = cast(AssistantMessageItem, new_history[1])
560-
assert cast(AssistantText, updated_result.content[0]).text == "Updated Second"
560+
assert updated_result.content[0].text == "Updated Second"
561561

562562
# Other items should be unchanged
563563
item1_result = cast(AssistantMessageItem, new_history[0])
564564
item3_result = cast(AssistantMessageItem, new_history[2])
565-
assert cast(AssistantText, item1_result.content[0]).text == "First"
566-
assert cast(AssistantText, item3_result.content[0]).text == "Third"
565+
assert item1_result.content[0].text == "First"
566+
assert item3_result.content[0].text == "Third"
567567

568568
def test_insert_new_item_after_previous_item(self):
569569
"""Test inserting new item after specified previous_item_id"""
@@ -598,7 +598,7 @@ def test_insert_new_item_after_previous_item(self):
598598

599599
# Content should be correct
600600
item2_result = cast(AssistantMessageItem, new_history[1])
601-
assert cast(AssistantText, item2_result.content[0]).text == "Second"
601+
assert item2_result.content[0].text == "Second"
602602

603603
def test_insert_new_item_after_nonexistent_previous_item(self):
604604
"""Test that item with nonexistent previous_item_id gets added to end"""
@@ -701,7 +701,7 @@ def test_complex_insertion_scenario(self):
701701
assert len(history) == 4
702702
assert [item.item_id for item in history] == ["A", "B", "D", "C"]
703703
itemB_result = cast(AssistantMessageItem, history[1])
704-
assert cast(AssistantText, itemB_result.content[0]).text == "Updated B"
704+
assert itemB_result.content[0].text == "Updated B"
705705

706706

707707
# Test 3: Tool call execution flow (_handle_tool_call method)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)