Skip to content

Commit c261c1a

Browse files
authored
Merge branch 'main' into feat/immediate-tool-call-item-streaming
2 parents 4200b51 + bad88e7 commit c261c1a

File tree

10 files changed

+371
-48
lines changed

10 files changed

+371
-48
lines changed

examples/realtime/app/agent.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,93 @@
11
from agents import function_tool
2-
from agents.realtime import RealtimeAgent
2+
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
3+
from agents.realtime import RealtimeAgent, realtime_handoff
34

45
"""
56
When running the UI example locally, you can edit this file to change the setup. THe server
67
will use the agent returned from get_starting_agent() as the starting agent."""
78

9+
### TOOLS
10+
11+
12+
@function_tool(
13+
name_override="faq_lookup_tool", description_override="Lookup frequently asked questions."
14+
)
15+
async def faq_lookup_tool(question: str) -> str:
16+
if "bag" in question or "baggage" in question:
17+
return (
18+
"You are allowed to bring one bag on the plane. "
19+
"It must be under 50 pounds and 22 inches x 14 inches x 9 inches."
20+
)
21+
elif "seats" in question or "plane" in question:
22+
return (
23+
"There are 120 seats on the plane. "
24+
"There are 22 business class seats and 98 economy seats. "
25+
"Exit rows are rows 4 and 16. "
26+
"Rows 5-8 are Economy Plus, with extra legroom. "
27+
)
28+
elif "wifi" in question:
29+
return "We have free wifi on the plane, join Airline-Wifi"
30+
return "I'm sorry, I don't know the answer to that question."
31+
32+
33+
@function_tool
34+
async def update_seat(confirmation_number: str, new_seat: str) -> str:
35+
"""
36+
Update the seat for a given confirmation number.
37+
38+
Args:
39+
confirmation_number: The confirmation number for the flight.
40+
new_seat: The new seat to update to.
41+
"""
42+
return f"Updated seat to {new_seat} for confirmation number {confirmation_number}"
43+
844

945
@function_tool
1046
def get_weather(city: str) -> str:
1147
"""Get the weather in a city."""
1248
return f"The weather in {city} is sunny."
1349

1450

15-
@function_tool
16-
def get_secret_number() -> int:
17-
"""Returns the secret number, if the user asks for it."""
18-
return 71
19-
51+
faq_agent = RealtimeAgent(
52+
name="FAQ Agent",
53+
handoff_description="A helpful agent that can answer questions about the airline.",
54+
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
55+
You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
56+
Use the following routine to support the customer.
57+
# Routine
58+
1. Identify the last question asked by the customer.
59+
2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge.
60+
3. If you cannot answer the question, transfer back to the triage agent.""",
61+
tools=[faq_lookup_tool],
62+
)
2063

21-
haiku_agent = RealtimeAgent(
22-
name="Haiku Agent",
23-
instructions="You are a haiku poet. You must respond ONLY in traditional haiku format (5-7-5 syllables). Every response should be a proper haiku about the topic. Do not break character.",
24-
tools=[],
64+
seat_booking_agent = RealtimeAgent(
65+
name="Seat Booking Agent",
66+
handoff_description="A helpful agent that can update a seat on a flight.",
67+
instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
68+
You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent.
69+
Use the following routine to support the customer.
70+
# Routine
71+
1. Ask for their confirmation number.
72+
2. Ask the customer what their desired seat number is.
73+
3. Use the update seat tool to update the seat on the flight.
74+
If the customer asks a question that is not related to the routine, transfer back to the triage agent. """,
75+
tools=[update_seat],
2576
)
2677

27-
assistant_agent = RealtimeAgent(
28-
name="Assistant",
29-
instructions="If the user wants poetry or haikus, you can hand them off to the haiku agent via the transfer_to_haiku_agent tool.",
30-
tools=[get_weather, get_secret_number],
31-
handoffs=[haiku_agent],
78+
triage_agent = RealtimeAgent(
79+
name="Triage Agent",
80+
handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.",
81+
instructions=(
82+
f"{RECOMMENDED_PROMPT_PREFIX} "
83+
"You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents."
84+
),
85+
handoffs=[faq_agent, realtime_handoff(seat_booking_agent)],
3286
)
3387

88+
faq_agent.handoffs.append(triage_agent)
89+
seat_booking_agent.handoffs.append(triage_agent)
90+
3491

3592
def get_starting_agent() -> RealtimeAgent:
36-
return assistant_agent
93+
return triage_agent

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[project]
22
name = "openai-agents"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
description = "OpenAI Agents SDK"
55
readme = "README.md"
66
requires-python = ">=3.9"
77
license = "MIT"
88
authors = [{ name = "OpenAI", email = "[email protected]" }]
99
dependencies = [
10-
"openai>=1.97.1,<2",
10+
"openai>=1.99.6,<2",
1111
"pydantic>=2.10, <3",
1212
"griffe>=1.5.6, <2",
1313
"typing-extensions>=4.12.2, <5",

src/agents/agent.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,119 @@ class Agent(AgentBase, Generic[TContext]):
223223
"""Whether to reset the tool choice to the default value after a tool has been called. Defaults
224224
to True. This ensures that the agent doesn't enter an infinite loop of tool usage."""
225225

226+
def __post_init__(self):
227+
from typing import get_origin
228+
229+
if not isinstance(self.name, str):
230+
raise TypeError(f"Agent name must be a string, got {type(self.name).__name__}")
231+
232+
if self.handoff_description is not None and not isinstance(self.handoff_description, str):
233+
raise TypeError(
234+
f"Agent handoff_description must be a string or None, "
235+
f"got {type(self.handoff_description).__name__}"
236+
)
237+
238+
if not isinstance(self.tools, list):
239+
raise TypeError(f"Agent tools must be a list, got {type(self.tools).__name__}")
240+
241+
if not isinstance(self.mcp_servers, list):
242+
raise TypeError(
243+
f"Agent mcp_servers must be a list, got {type(self.mcp_servers).__name__}"
244+
)
245+
246+
if not isinstance(self.mcp_config, dict):
247+
raise TypeError(
248+
f"Agent mcp_config must be a dict, got {type(self.mcp_config).__name__}"
249+
)
250+
251+
if (
252+
self.instructions is not None
253+
and not isinstance(self.instructions, str)
254+
and not callable(self.instructions)
255+
):
256+
raise TypeError(
257+
f"Agent instructions must be a string, callable, or None, "
258+
f"got {type(self.instructions).__name__}"
259+
)
260+
261+
if (
262+
self.prompt is not None
263+
and not callable(self.prompt)
264+
and not hasattr(self.prompt, "get")
265+
):
266+
raise TypeError(
267+
f"Agent prompt must be a Prompt, DynamicPromptFunction, or None, "
268+
f"got {type(self.prompt).__name__}"
269+
)
270+
271+
if not isinstance(self.handoffs, list):
272+
raise TypeError(f"Agent handoffs must be a list, got {type(self.handoffs).__name__}")
273+
274+
if self.model is not None and not isinstance(self.model, str):
275+
from .models.interface import Model
276+
277+
if not isinstance(self.model, Model):
278+
raise TypeError(
279+
f"Agent model must be a string, Model, or None, got {type(self.model).__name__}"
280+
)
281+
282+
if not isinstance(self.model_settings, ModelSettings):
283+
raise TypeError(
284+
f"Agent model_settings must be a ModelSettings instance, "
285+
f"got {type(self.model_settings).__name__}"
286+
)
287+
288+
if not isinstance(self.input_guardrails, list):
289+
raise TypeError(
290+
f"Agent input_guardrails must be a list, got {type(self.input_guardrails).__name__}"
291+
)
292+
293+
if not isinstance(self.output_guardrails, list):
294+
raise TypeError(
295+
f"Agent output_guardrails must be a list, "
296+
f"got {type(self.output_guardrails).__name__}"
297+
)
298+
299+
if self.output_type is not None:
300+
from .agent_output import AgentOutputSchemaBase
301+
302+
if not (
303+
isinstance(self.output_type, (type, AgentOutputSchemaBase))
304+
or get_origin(self.output_type) is not None
305+
):
306+
raise TypeError(
307+
f"Agent output_type must be a type, AgentOutputSchemaBase, or None, "
308+
f"got {type(self.output_type).__name__}"
309+
)
310+
311+
if self.hooks is not None:
312+
from .lifecycle import AgentHooksBase
313+
314+
if not isinstance(self.hooks, AgentHooksBase):
315+
raise TypeError(
316+
f"Agent hooks must be an AgentHooks instance or None, "
317+
f"got {type(self.hooks).__name__}"
318+
)
319+
320+
if (
321+
not (
322+
isinstance(self.tool_use_behavior, str)
323+
and self.tool_use_behavior in ["run_llm_again", "stop_on_first_tool"]
324+
)
325+
and not isinstance(self.tool_use_behavior, dict)
326+
and not callable(self.tool_use_behavior)
327+
):
328+
raise TypeError(
329+
f"Agent tool_use_behavior must be 'run_llm_again', 'stop_on_first_tool', "
330+
f"StopAtTools dict, or callable, got {type(self.tool_use_behavior).__name__}"
331+
)
332+
333+
if not isinstance(self.reset_tool_choice, bool):
334+
raise TypeError(
335+
f"Agent reset_tool_choice must be a boolean, "
336+
f"got {type(self.reset_tool_choice).__name__}"
337+
)
338+
226339
def clone(self, **kwargs: Any) -> Agent[TContext]:
227340
"""Make a copy of the agent, with the given arguments changed.
228341
Notes:

src/agents/extensions/models/litellm_model.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
) from _e
1919

2020
from openai import NOT_GIVEN, AsyncStream, NotGiven
21-
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageToolCall
21+
from openai.types.chat import (
22+
ChatCompletionChunk,
23+
ChatCompletionMessageFunctionToolCall,
24+
)
2225
from openai.types.chat.chat_completion_message import (
2326
Annotation,
2427
AnnotationURLCitation,
2528
ChatCompletionMessage,
2629
)
27-
from openai.types.chat.chat_completion_message_tool_call import Function
30+
from openai.types.chat.chat_completion_message_function_tool_call import Function
31+
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall
2832
from openai.types.responses import Response
2933

3034
from ... import _debug
@@ -362,7 +366,7 @@ def convert_message_to_openai(
362366
if message.role != "assistant":
363367
raise ModelBehaviorError(f"Unsupported role: {message.role}")
364368

365-
tool_calls = (
369+
tool_calls: list[ChatCompletionMessageToolCall] | None = (
366370
[LitellmConverter.convert_tool_call_to_openai(tool) for tool in message.tool_calls]
367371
if message.tool_calls
368372
else None
@@ -413,11 +417,12 @@ def convert_annotations_to_openai(
413417
@classmethod
414418
def convert_tool_call_to_openai(
415419
cls, tool_call: litellm.types.utils.ChatCompletionMessageToolCall
416-
) -> ChatCompletionMessageToolCall:
417-
return ChatCompletionMessageToolCall(
420+
) -> ChatCompletionMessageFunctionToolCall:
421+
return ChatCompletionMessageFunctionToolCall(
418422
id=tool_call.id,
419423
type="function",
420424
function=Function(
421-
name=tool_call.function.name or "", arguments=tool_call.function.arguments
425+
name=tool_call.function.name or "",
426+
arguments=tool_call.function.arguments,
422427
),
423428
)

src/agents/models/chatcmpl_converter.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
ChatCompletionContentPartTextParam,
1313
ChatCompletionDeveloperMessageParam,
1414
ChatCompletionMessage,
15+
ChatCompletionMessageFunctionToolCallParam,
1516
ChatCompletionMessageParam,
16-
ChatCompletionMessageToolCallParam,
1717
ChatCompletionSystemMessageParam,
1818
ChatCompletionToolChoiceOptionParam,
1919
ChatCompletionToolMessageParam,
@@ -126,15 +126,18 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon
126126

127127
if message.tool_calls:
128128
for tool_call in message.tool_calls:
129-
items.append(
130-
ResponseFunctionToolCall(
131-
id=FAKE_RESPONSES_ID,
132-
call_id=tool_call.id,
133-
arguments=tool_call.function.arguments,
134-
name=tool_call.function.name,
135-
type="function_call",
129+
if tool_call.type == "function":
130+
items.append(
131+
ResponseFunctionToolCall(
132+
id=FAKE_RESPONSES_ID,
133+
call_id=tool_call.id,
134+
arguments=tool_call.function.arguments,
135+
name=tool_call.function.name,
136+
type="function_call",
137+
)
136138
)
137-
)
139+
elif tool_call.type == "custom":
140+
pass
138141

139142
return items
140143

@@ -420,7 +423,7 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
420423
elif file_search := cls.maybe_file_search_call(item):
421424
asst = ensure_assistant_message()
422425
tool_calls = list(asst.get("tool_calls", []))
423-
new_tool_call = ChatCompletionMessageToolCallParam(
426+
new_tool_call = ChatCompletionMessageFunctionToolCallParam(
424427
id=file_search["id"],
425428
type="function",
426429
function={
@@ -440,7 +443,7 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
440443
asst = ensure_assistant_message()
441444
tool_calls = list(asst.get("tool_calls", []))
442445
arguments = func_call["arguments"] if func_call["arguments"] else "{}"
443-
new_tool_call = ChatCompletionMessageToolCallParam(
446+
new_tool_call = ChatCompletionMessageFunctionToolCallParam(
444447
id=func_call["call_id"],
445448
type="function",
446449
function={

0 commit comments

Comments
 (0)