openai-agents 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openai-agents might be problematic. Click here for more details.
- agents/agent.py +1 -1
- agents/agent_output.py +2 -2
- agents/guardrail.py +1 -1
- agents/handoffs.py +32 -14
- agents/mcp/server.py +39 -0
- agents/mcp/util.py +11 -3
- agents/models/chatcmpl_converter.py +1 -1
- agents/models/chatcmpl_stream_handler.py +134 -43
- agents/models/openai_responses.py +1 -1
- agents/realtime/__init__.py +3 -0
- agents/realtime/agent.py +10 -1
- agents/realtime/config.py +60 -0
- agents/realtime/handoffs.py +165 -0
- agents/realtime/items.py +94 -1
- agents/realtime/openai_realtime.py +186 -100
- agents/realtime/session.py +38 -5
- {openai_agents-0.2.0.dist-info → openai_agents-0.2.1.dist-info}/METADATA +3 -3
- {openai_agents-0.2.0.dist-info → openai_agents-0.2.1.dist-info}/RECORD +20 -19
- {openai_agents-0.2.0.dist-info → openai_agents-0.2.1.dist-info}/WHEEL +0 -0
- {openai_agents-0.2.0.dist-info → openai_agents-0.2.1.dist-info}/licenses/LICENSE +0 -0
agents/agent.py
CHANGED
|
@@ -158,7 +158,7 @@ class Agent(AgentBase, Generic[TContext]):
|
|
|
158
158
|
usable with OpenAI models, using the Responses API.
|
|
159
159
|
"""
|
|
160
160
|
|
|
161
|
-
handoffs: list[Agent[Any] | Handoff[TContext]] = field(default_factory=list)
|
|
161
|
+
handoffs: list[Agent[Any] | Handoff[TContext, Any]] = field(default_factory=list)
|
|
162
162
|
"""Handoffs are sub-agents that the agent can delegate to. You can provide a list of handoffs,
|
|
163
163
|
and the agent can choose to delegate to them if relevant. Allows for separation of concerns and
|
|
164
164
|
modularity.
|
agents/agent_output.py
CHANGED
|
@@ -115,8 +115,8 @@ class AgentOutputSchema(AgentOutputSchemaBase):
|
|
|
115
115
|
except UserError as e:
|
|
116
116
|
raise UserError(
|
|
117
117
|
"Strict JSON schema is enabled, but the output type is not valid. "
|
|
118
|
-
"Either make the output type strict,
|
|
119
|
-
"your
|
|
118
|
+
"Either make the output type strict, "
|
|
119
|
+
"or wrap your type with AgentOutputSchema(your_type, strict_json_schema=False)"
|
|
120
120
|
) from e
|
|
121
121
|
|
|
122
122
|
def is_plain_text(self) -> bool:
|
agents/guardrail.py
CHANGED
|
@@ -244,7 +244,7 @@ def input_guardrail(
|
|
|
244
244
|
return InputGuardrail(
|
|
245
245
|
guardrail_function=f,
|
|
246
246
|
# If not set, guardrail name uses the function’s name by default.
|
|
247
|
-
name=name if name else f.__name__
|
|
247
|
+
name=name if name else f.__name__,
|
|
248
248
|
)
|
|
249
249
|
|
|
250
250
|
if func is not None:
|
agents/handoffs.py
CHANGED
|
@@ -18,12 +18,15 @@ from .util import _error_tracing, _json, _transforms
|
|
|
18
18
|
from .util._types import MaybeAwaitable
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
from .agent import Agent
|
|
21
|
+
from .agent import Agent, AgentBase
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
# The handoff input type is the type of data passed when the agent is called via a handoff.
|
|
25
25
|
THandoffInput = TypeVar("THandoffInput", default=Any)
|
|
26
26
|
|
|
27
|
+
# The agent type that the handoff returns
|
|
28
|
+
TAgent = TypeVar("TAgent", bound="AgentBase[Any]", default="Agent[Any]")
|
|
29
|
+
|
|
27
30
|
OnHandoffWithInput = Callable[[RunContextWrapper[Any], THandoffInput], Any]
|
|
28
31
|
OnHandoffWithoutInput = Callable[[RunContextWrapper[Any]], Any]
|
|
29
32
|
|
|
@@ -52,7 +55,7 @@ HandoffInputFilter: TypeAlias = Callable[[HandoffInputData], HandoffInputData]
|
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
@dataclass
|
|
55
|
-
class Handoff(Generic[TContext]):
|
|
58
|
+
class Handoff(Generic[TContext, TAgent]):
|
|
56
59
|
"""A handoff is when an agent delegates a task to another agent.
|
|
57
60
|
For example, in a customer support scenario you might have a "triage agent" that determines
|
|
58
61
|
which agent should handle the user's request, and sub-agents that specialize in different
|
|
@@ -69,7 +72,7 @@ class Handoff(Generic[TContext]):
|
|
|
69
72
|
"""The JSON schema for the handoff input. Can be empty if the handoff does not take an input.
|
|
70
73
|
"""
|
|
71
74
|
|
|
72
|
-
on_invoke_handoff: Callable[[RunContextWrapper[Any], str], Awaitable[
|
|
75
|
+
on_invoke_handoff: Callable[[RunContextWrapper[Any], str], Awaitable[TAgent]]
|
|
73
76
|
"""The function that invokes the handoff. The parameters passed are:
|
|
74
77
|
1. The handoff run context
|
|
75
78
|
2. The arguments from the LLM, as a JSON string. Empty string if input_json_schema is empty.
|
|
@@ -100,20 +103,22 @@ class Handoff(Generic[TContext]):
|
|
|
100
103
|
True, as it increases the likelihood of correct JSON input.
|
|
101
104
|
"""
|
|
102
105
|
|
|
103
|
-
is_enabled: bool | Callable[[RunContextWrapper[Any],
|
|
106
|
+
is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase[Any]], MaybeAwaitable[bool]] = (
|
|
107
|
+
True
|
|
108
|
+
)
|
|
104
109
|
"""Whether the handoff is enabled. Either a bool or a Callable that takes the run context and
|
|
105
110
|
agent and returns whether the handoff is enabled. You can use this to dynamically enable/disable
|
|
106
111
|
a handoff based on your context/state."""
|
|
107
112
|
|
|
108
|
-
def get_transfer_message(self, agent:
|
|
113
|
+
def get_transfer_message(self, agent: AgentBase[Any]) -> str:
|
|
109
114
|
return json.dumps({"assistant": agent.name})
|
|
110
115
|
|
|
111
116
|
@classmethod
|
|
112
|
-
def default_tool_name(cls, agent:
|
|
117
|
+
def default_tool_name(cls, agent: AgentBase[Any]) -> str:
|
|
113
118
|
return _transforms.transform_string_function_style(f"transfer_to_{agent.name}")
|
|
114
119
|
|
|
115
120
|
@classmethod
|
|
116
|
-
def default_tool_description(cls, agent:
|
|
121
|
+
def default_tool_description(cls, agent: AgentBase[Any]) -> str:
|
|
117
122
|
return (
|
|
118
123
|
f"Handoff to the {agent.name} agent to handle the request. "
|
|
119
124
|
f"{agent.handoff_description or ''}"
|
|
@@ -128,7 +133,7 @@ def handoff(
|
|
|
128
133
|
tool_description_override: str | None = None,
|
|
129
134
|
input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
|
|
130
135
|
is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
|
|
131
|
-
) -> Handoff[TContext]: ...
|
|
136
|
+
) -> Handoff[TContext, Agent[TContext]]: ...
|
|
132
137
|
|
|
133
138
|
|
|
134
139
|
@overload
|
|
@@ -141,7 +146,7 @@ def handoff(
|
|
|
141
146
|
tool_name_override: str | None = None,
|
|
142
147
|
input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
|
|
143
148
|
is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
|
|
144
|
-
) -> Handoff[TContext]: ...
|
|
149
|
+
) -> Handoff[TContext, Agent[TContext]]: ...
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
@overload
|
|
@@ -153,7 +158,7 @@ def handoff(
|
|
|
153
158
|
tool_name_override: str | None = None,
|
|
154
159
|
input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
|
|
155
160
|
is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True,
|
|
156
|
-
) -> Handoff[TContext]: ...
|
|
161
|
+
) -> Handoff[TContext, Agent[TContext]]: ...
|
|
157
162
|
|
|
158
163
|
|
|
159
164
|
def handoff(
|
|
@@ -163,8 +168,9 @@ def handoff(
|
|
|
163
168
|
on_handoff: OnHandoffWithInput[THandoffInput] | OnHandoffWithoutInput | None = None,
|
|
164
169
|
input_type: type[THandoffInput] | None = None,
|
|
165
170
|
input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None,
|
|
166
|
-
is_enabled: bool
|
|
167
|
-
|
|
171
|
+
is_enabled: bool
|
|
172
|
+
| Callable[[RunContextWrapper[Any], Agent[TContext]], MaybeAwaitable[bool]] = True,
|
|
173
|
+
) -> Handoff[TContext, Agent[TContext]]:
|
|
168
174
|
"""Create a handoff from an agent.
|
|
169
175
|
|
|
170
176
|
Args:
|
|
@@ -202,7 +208,7 @@ def handoff(
|
|
|
202
208
|
|
|
203
209
|
async def _invoke_handoff(
|
|
204
210
|
ctx: RunContextWrapper[Any], input_json: str | None = None
|
|
205
|
-
) -> Agent[
|
|
211
|
+
) -> Agent[TContext]:
|
|
206
212
|
if input_type is not None and type_adapter is not None:
|
|
207
213
|
if input_json is None:
|
|
208
214
|
_error_tracing.attach_error_to_current_span(
|
|
@@ -239,6 +245,18 @@ def handoff(
|
|
|
239
245
|
# If there is a need, we can make this configurable in the future
|
|
240
246
|
input_json_schema = ensure_strict_json_schema(input_json_schema)
|
|
241
247
|
|
|
248
|
+
async def _is_enabled(ctx: RunContextWrapper[Any], agent_base: AgentBase[Any]) -> bool:
|
|
249
|
+
from .agent import Agent
|
|
250
|
+
|
|
251
|
+
assert callable(is_enabled), "is_enabled must be non-null here"
|
|
252
|
+
assert isinstance(agent_base, Agent), "Can't handoff to a non-Agent"
|
|
253
|
+
result = is_enabled(ctx, agent_base)
|
|
254
|
+
|
|
255
|
+
if inspect.isawaitable(result):
|
|
256
|
+
return await result
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
242
260
|
return Handoff(
|
|
243
261
|
tool_name=tool_name,
|
|
244
262
|
tool_description=tool_description,
|
|
@@ -246,5 +264,5 @@ def handoff(
|
|
|
246
264
|
on_invoke_handoff=_invoke_handoff,
|
|
247
265
|
input_filter=input_filter,
|
|
248
266
|
agent_name=agent.name,
|
|
249
|
-
is_enabled=is_enabled,
|
|
267
|
+
is_enabled=_is_enabled if callable(is_enabled) else is_enabled,
|
|
250
268
|
)
|
agents/mcp/server.py
CHANGED
|
@@ -28,6 +28,17 @@ if TYPE_CHECKING:
|
|
|
28
28
|
class MCPServer(abc.ABC):
|
|
29
29
|
"""Base class for Model Context Protocol servers."""
|
|
30
30
|
|
|
31
|
+
def __init__(self, use_structured_content: bool = False):
|
|
32
|
+
"""
|
|
33
|
+
Args:
|
|
34
|
+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
|
|
35
|
+
MCP tool.Defaults to False for backwards compatibility - most MCP servers still
|
|
36
|
+
include the structured content in the `tool_result.content`, and using it by
|
|
37
|
+
default will cause duplicate content. You can set this to True if you know the
|
|
38
|
+
server will not duplicate the structured content in the `tool_result.content`.
|
|
39
|
+
"""
|
|
40
|
+
self.use_structured_content = use_structured_content
|
|
41
|
+
|
|
31
42
|
@abc.abstractmethod
|
|
32
43
|
async def connect(self):
|
|
33
44
|
"""Connect to the server. For example, this might mean spawning a subprocess or
|
|
@@ -86,6 +97,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
86
97
|
cache_tools_list: bool,
|
|
87
98
|
client_session_timeout_seconds: float | None,
|
|
88
99
|
tool_filter: ToolFilter = None,
|
|
100
|
+
use_structured_content: bool = False,
|
|
89
101
|
):
|
|
90
102
|
"""
|
|
91
103
|
Args:
|
|
@@ -98,7 +110,13 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
98
110
|
|
|
99
111
|
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
|
|
100
112
|
tool_filter: The tool filter to use for filtering tools.
|
|
113
|
+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
|
|
114
|
+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
|
|
115
|
+
include the structured content in the `tool_result.content`, and using it by
|
|
116
|
+
default will cause duplicate content. You can set this to True if you know the
|
|
117
|
+
server will not duplicate the structured content in the `tool_result.content`.
|
|
101
118
|
"""
|
|
119
|
+
super().__init__(use_structured_content=use_structured_content)
|
|
102
120
|
self.session: ClientSession | None = None
|
|
103
121
|
self.exit_stack: AsyncExitStack = AsyncExitStack()
|
|
104
122
|
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
|
|
@@ -346,6 +364,7 @@ class MCPServerStdio(_MCPServerWithClientSession):
|
|
|
346
364
|
name: str | None = None,
|
|
347
365
|
client_session_timeout_seconds: float | None = 5,
|
|
348
366
|
tool_filter: ToolFilter = None,
|
|
367
|
+
use_structured_content: bool = False,
|
|
349
368
|
):
|
|
350
369
|
"""Create a new MCP server based on the stdio transport.
|
|
351
370
|
|
|
@@ -364,11 +383,17 @@ class MCPServerStdio(_MCPServerWithClientSession):
|
|
|
364
383
|
command.
|
|
365
384
|
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
|
|
366
385
|
tool_filter: The tool filter to use for filtering tools.
|
|
386
|
+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
|
|
387
|
+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
|
|
388
|
+
include the structured content in the `tool_result.content`, and using it by
|
|
389
|
+
default will cause duplicate content. You can set this to True if you know the
|
|
390
|
+
server will not duplicate the structured content in the `tool_result.content`.
|
|
367
391
|
"""
|
|
368
392
|
super().__init__(
|
|
369
393
|
cache_tools_list,
|
|
370
394
|
client_session_timeout_seconds,
|
|
371
395
|
tool_filter,
|
|
396
|
+
use_structured_content,
|
|
372
397
|
)
|
|
373
398
|
|
|
374
399
|
self.params = StdioServerParameters(
|
|
@@ -429,6 +454,7 @@ class MCPServerSse(_MCPServerWithClientSession):
|
|
|
429
454
|
name: str | None = None,
|
|
430
455
|
client_session_timeout_seconds: float | None = 5,
|
|
431
456
|
tool_filter: ToolFilter = None,
|
|
457
|
+
use_structured_content: bool = False,
|
|
432
458
|
):
|
|
433
459
|
"""Create a new MCP server based on the HTTP with SSE transport.
|
|
434
460
|
|
|
@@ -449,11 +475,17 @@ class MCPServerSse(_MCPServerWithClientSession):
|
|
|
449
475
|
|
|
450
476
|
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
|
|
451
477
|
tool_filter: The tool filter to use for filtering tools.
|
|
478
|
+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
|
|
479
|
+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
|
|
480
|
+
include the structured content in the `tool_result.content`, and using it by
|
|
481
|
+
default will cause duplicate content. You can set this to True if you know the
|
|
482
|
+
server will not duplicate the structured content in the `tool_result.content`.
|
|
452
483
|
"""
|
|
453
484
|
super().__init__(
|
|
454
485
|
cache_tools_list,
|
|
455
486
|
client_session_timeout_seconds,
|
|
456
487
|
tool_filter,
|
|
488
|
+
use_structured_content,
|
|
457
489
|
)
|
|
458
490
|
|
|
459
491
|
self.params = params
|
|
@@ -514,6 +546,7 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
514
546
|
name: str | None = None,
|
|
515
547
|
client_session_timeout_seconds: float | None = 5,
|
|
516
548
|
tool_filter: ToolFilter = None,
|
|
549
|
+
use_structured_content: bool = False,
|
|
517
550
|
):
|
|
518
551
|
"""Create a new MCP server based on the Streamable HTTP transport.
|
|
519
552
|
|
|
@@ -535,11 +568,17 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
535
568
|
|
|
536
569
|
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
|
|
537
570
|
tool_filter: The tool filter to use for filtering tools.
|
|
571
|
+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
|
|
572
|
+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
|
|
573
|
+
include the structured content in the `tool_result.content`, and using it by
|
|
574
|
+
default will cause duplicate content. You can set this to True if you know the
|
|
575
|
+
server will not duplicate the structured content in the `tool_result.content`.
|
|
538
576
|
"""
|
|
539
577
|
super().__init__(
|
|
540
578
|
cache_tools_list,
|
|
541
579
|
client_session_timeout_seconds,
|
|
542
580
|
tool_filter,
|
|
581
|
+
use_structured_content,
|
|
543
582
|
)
|
|
544
583
|
|
|
545
584
|
self.params = params
|
agents/mcp/util.py
CHANGED
|
@@ -198,11 +198,19 @@ class MCPUtil:
|
|
|
198
198
|
# string. We'll try to convert.
|
|
199
199
|
if len(result.content) == 1:
|
|
200
200
|
tool_output = result.content[0].model_dump_json()
|
|
201
|
+
# Append structured content if it exists and we're using it.
|
|
202
|
+
if server.use_structured_content and result.structuredContent:
|
|
203
|
+
tool_output = f"{tool_output}\n{json.dumps(result.structuredContent)}"
|
|
201
204
|
elif len(result.content) > 1:
|
|
202
|
-
|
|
205
|
+
tool_results = [item.model_dump(mode="json") for item in result.content]
|
|
206
|
+
if server.use_structured_content and result.structuredContent:
|
|
207
|
+
tool_results.append(result.structuredContent)
|
|
208
|
+
tool_output = json.dumps(tool_results)
|
|
209
|
+
elif server.use_structured_content and result.structuredContent:
|
|
210
|
+
tool_output = json.dumps(result.structuredContent)
|
|
203
211
|
else:
|
|
204
|
-
|
|
205
|
-
tool_output = "
|
|
212
|
+
# Empty content is a valid result (e.g., "no results found")
|
|
213
|
+
tool_output = "[]"
|
|
206
214
|
|
|
207
215
|
current_span = get_current_span()
|
|
208
216
|
if current_span:
|
|
@@ -484,7 +484,7 @@ class Converter:
|
|
|
484
484
|
)
|
|
485
485
|
|
|
486
486
|
@classmethod
|
|
487
|
-
def convert_handoff_tool(cls, handoff: Handoff[Any]) -> ChatCompletionToolParam:
|
|
487
|
+
def convert_handoff_tool(cls, handoff: Handoff[Any, Any]) -> ChatCompletionToolParam:
|
|
488
488
|
return {
|
|
489
489
|
"type": "function",
|
|
490
490
|
"function": {
|
|
@@ -53,6 +53,9 @@ class StreamingState:
|
|
|
53
53
|
refusal_content_index_and_output: tuple[int, ResponseOutputRefusal] | None = None
|
|
54
54
|
reasoning_content_index_and_output: tuple[int, ResponseReasoningItem] | None = None
|
|
55
55
|
function_calls: dict[int, ResponseFunctionToolCall] = field(default_factory=dict)
|
|
56
|
+
# Fields for real-time function call streaming
|
|
57
|
+
function_call_streaming: dict[int, bool] = field(default_factory=dict)
|
|
58
|
+
function_call_output_idx: dict[int, int] = field(default_factory=dict)
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
class SequenceNumber:
|
|
@@ -255,9 +258,7 @@ class ChatCmplStreamHandler:
|
|
|
255
258
|
# Accumulate the refusal string in the output part
|
|
256
259
|
state.refusal_content_index_and_output[1].refusal += delta.refusal
|
|
257
260
|
|
|
258
|
-
# Handle tool calls
|
|
259
|
-
# Because we don't know the name of the function until the end of the stream, we'll
|
|
260
|
-
# save everything and yield events at the end
|
|
261
|
+
# Handle tool calls with real-time streaming support
|
|
261
262
|
if delta.tool_calls:
|
|
262
263
|
for tc_delta in delta.tool_calls:
|
|
263
264
|
if tc_delta.index not in state.function_calls:
|
|
@@ -268,15 +269,76 @@ class ChatCmplStreamHandler:
|
|
|
268
269
|
type="function_call",
|
|
269
270
|
call_id="",
|
|
270
271
|
)
|
|
272
|
+
state.function_call_streaming[tc_delta.index] = False
|
|
273
|
+
|
|
271
274
|
tc_function = tc_delta.function
|
|
272
275
|
|
|
276
|
+
# Accumulate arguments as they come in
|
|
273
277
|
state.function_calls[tc_delta.index].arguments += (
|
|
274
278
|
tc_function.arguments if tc_function else ""
|
|
275
279
|
) or ""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
|
|
281
|
+
# Set function name directly (it's correct from the first function call chunk)
|
|
282
|
+
if tc_function and tc_function.name:
|
|
283
|
+
state.function_calls[tc_delta.index].name = tc_function.name
|
|
284
|
+
|
|
285
|
+
if tc_delta.id:
|
|
286
|
+
state.function_calls[tc_delta.index].call_id = tc_delta.id
|
|
287
|
+
|
|
288
|
+
function_call = state.function_calls[tc_delta.index]
|
|
289
|
+
|
|
290
|
+
# Start streaming as soon as we have function name and call_id
|
|
291
|
+
if (not state.function_call_streaming[tc_delta.index] and
|
|
292
|
+
function_call.name and
|
|
293
|
+
function_call.call_id):
|
|
294
|
+
|
|
295
|
+
# Calculate the output index for this function call
|
|
296
|
+
function_call_starting_index = 0
|
|
297
|
+
if state.reasoning_content_index_and_output:
|
|
298
|
+
function_call_starting_index += 1
|
|
299
|
+
if state.text_content_index_and_output:
|
|
300
|
+
function_call_starting_index += 1
|
|
301
|
+
if state.refusal_content_index_and_output:
|
|
302
|
+
function_call_starting_index += 1
|
|
303
|
+
|
|
304
|
+
# Add offset for already started function calls
|
|
305
|
+
function_call_starting_index += sum(
|
|
306
|
+
1 for streaming in state.function_call_streaming.values() if streaming
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Mark this function call as streaming and store its output index
|
|
310
|
+
state.function_call_streaming[tc_delta.index] = True
|
|
311
|
+
state.function_call_output_idx[
|
|
312
|
+
tc_delta.index
|
|
313
|
+
] = function_call_starting_index
|
|
314
|
+
|
|
315
|
+
# Send initial function call added event
|
|
316
|
+
yield ResponseOutputItemAddedEvent(
|
|
317
|
+
item=ResponseFunctionToolCall(
|
|
318
|
+
id=FAKE_RESPONSES_ID,
|
|
319
|
+
call_id=function_call.call_id,
|
|
320
|
+
arguments="", # Start with empty arguments
|
|
321
|
+
name=function_call.name,
|
|
322
|
+
type="function_call",
|
|
323
|
+
),
|
|
324
|
+
output_index=function_call_starting_index,
|
|
325
|
+
type="response.output_item.added",
|
|
326
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Stream arguments if we've started streaming this function call
|
|
330
|
+
if (state.function_call_streaming.get(tc_delta.index, False) and
|
|
331
|
+
tc_function and
|
|
332
|
+
tc_function.arguments):
|
|
333
|
+
|
|
334
|
+
output_index = state.function_call_output_idx[tc_delta.index]
|
|
335
|
+
yield ResponseFunctionCallArgumentsDeltaEvent(
|
|
336
|
+
delta=tc_function.arguments,
|
|
337
|
+
item_id=FAKE_RESPONSES_ID,
|
|
338
|
+
output_index=output_index,
|
|
339
|
+
type="response.function_call_arguments.delta",
|
|
340
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
341
|
+
)
|
|
280
342
|
|
|
281
343
|
if state.reasoning_content_index_and_output:
|
|
282
344
|
yield ResponseReasoningSummaryPartDoneEvent(
|
|
@@ -327,42 +389,71 @@ class ChatCmplStreamHandler:
|
|
|
327
389
|
sequence_number=sequence_number.get_and_increment(),
|
|
328
390
|
)
|
|
329
391
|
|
|
330
|
-
#
|
|
331
|
-
for function_call in state.function_calls.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
392
|
+
# Send completion events for function calls
|
|
393
|
+
for index, function_call in state.function_calls.items():
|
|
394
|
+
if state.function_call_streaming.get(index, False):
|
|
395
|
+
# Function call was streamed, just send the completion event
|
|
396
|
+
output_index = state.function_call_output_idx[index]
|
|
397
|
+
yield ResponseOutputItemDoneEvent(
|
|
398
|
+
item=ResponseFunctionToolCall(
|
|
399
|
+
id=FAKE_RESPONSES_ID,
|
|
400
|
+
call_id=function_call.call_id,
|
|
401
|
+
arguments=function_call.arguments,
|
|
402
|
+
name=function_call.name,
|
|
403
|
+
type="function_call",
|
|
404
|
+
),
|
|
405
|
+
output_index=output_index,
|
|
406
|
+
type="response.output_item.done",
|
|
407
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
# Function call was not streamed (fallback to old behavior)
|
|
411
|
+
# This handles edge cases where function name never arrived
|
|
412
|
+
fallback_starting_index = 0
|
|
413
|
+
if state.reasoning_content_index_and_output:
|
|
414
|
+
fallback_starting_index += 1
|
|
415
|
+
if state.text_content_index_and_output:
|
|
416
|
+
fallback_starting_index += 1
|
|
417
|
+
if state.refusal_content_index_and_output:
|
|
418
|
+
fallback_starting_index += 1
|
|
419
|
+
|
|
420
|
+
# Add offset for already started function calls
|
|
421
|
+
fallback_starting_index += sum(
|
|
422
|
+
1 for streaming in state.function_call_streaming.values() if streaming
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Send all events at once (backward compatibility)
|
|
426
|
+
yield ResponseOutputItemAddedEvent(
|
|
427
|
+
item=ResponseFunctionToolCall(
|
|
428
|
+
id=FAKE_RESPONSES_ID,
|
|
429
|
+
call_id=function_call.call_id,
|
|
430
|
+
arguments=function_call.arguments,
|
|
431
|
+
name=function_call.name,
|
|
432
|
+
type="function_call",
|
|
433
|
+
),
|
|
434
|
+
output_index=fallback_starting_index,
|
|
435
|
+
type="response.output_item.added",
|
|
436
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
437
|
+
)
|
|
438
|
+
yield ResponseFunctionCallArgumentsDeltaEvent(
|
|
439
|
+
delta=function_call.arguments,
|
|
440
|
+
item_id=FAKE_RESPONSES_ID,
|
|
441
|
+
output_index=fallback_starting_index,
|
|
442
|
+
type="response.function_call_arguments.delta",
|
|
443
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
444
|
+
)
|
|
445
|
+
yield ResponseOutputItemDoneEvent(
|
|
446
|
+
item=ResponseFunctionToolCall(
|
|
447
|
+
id=FAKE_RESPONSES_ID,
|
|
448
|
+
call_id=function_call.call_id,
|
|
449
|
+
arguments=function_call.arguments,
|
|
450
|
+
name=function_call.name,
|
|
451
|
+
type="function_call",
|
|
452
|
+
),
|
|
453
|
+
output_index=fallback_starting_index,
|
|
454
|
+
type="response.output_item.done",
|
|
455
|
+
sequence_number=sequence_number.get_and_increment(),
|
|
456
|
+
)
|
|
366
457
|
|
|
367
458
|
# Finally, send the Response completed event
|
|
368
459
|
outputs: list[ResponseOutputItem] = []
|
|
@@ -370,7 +370,7 @@ class Converter:
|
|
|
370
370
|
def convert_tools(
|
|
371
371
|
cls,
|
|
372
372
|
tools: list[Tool],
|
|
373
|
-
handoffs: list[Handoff[Any]],
|
|
373
|
+
handoffs: list[Handoff[Any, Any]],
|
|
374
374
|
) -> ConvertedTools:
|
|
375
375
|
converted_tools: list[ToolParam] = []
|
|
376
376
|
includes: list[ResponseIncludable] = []
|
agents/realtime/__init__.py
CHANGED
|
@@ -30,6 +30,7 @@ from .events import (
|
|
|
30
30
|
RealtimeToolEnd,
|
|
31
31
|
RealtimeToolStart,
|
|
32
32
|
)
|
|
33
|
+
from .handoffs import realtime_handoff
|
|
33
34
|
from .items import (
|
|
34
35
|
AssistantMessageItem,
|
|
35
36
|
AssistantText,
|
|
@@ -92,6 +93,8 @@ __all__ = [
|
|
|
92
93
|
"RealtimeAgentHooks",
|
|
93
94
|
"RealtimeRunHooks",
|
|
94
95
|
"RealtimeRunner",
|
|
96
|
+
# Handoffs
|
|
97
|
+
"realtime_handoff",
|
|
95
98
|
# Config
|
|
96
99
|
"RealtimeAudioFormat",
|
|
97
100
|
"RealtimeClientMessage",
|
agents/realtime/agent.py
CHANGED
|
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
import inspect
|
|
5
5
|
from collections.abc import Awaitable
|
|
6
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any, Callable, Generic, cast
|
|
8
8
|
|
|
9
9
|
from ..agent import AgentBase
|
|
10
|
+
from ..handoffs import Handoff
|
|
10
11
|
from ..lifecycle import AgentHooksBase, RunHooksBase
|
|
11
12
|
from ..logger import logger
|
|
12
13
|
from ..run_context import RunContextWrapper, TContext
|
|
@@ -53,6 +54,14 @@ class RealtimeAgent(AgentBase, Generic[TContext]):
|
|
|
53
54
|
return a string.
|
|
54
55
|
"""
|
|
55
56
|
|
|
57
|
+
handoffs: list[RealtimeAgent[Any] | Handoff[TContext, RealtimeAgent[Any]]] = field(
|
|
58
|
+
default_factory=list
|
|
59
|
+
)
|
|
60
|
+
"""Handoffs are sub-agents that the agent can delegate to. You can provide a list of handoffs,
|
|
61
|
+
and the agent can choose to delegate to them if relevant. Allows for separation of concerns and
|
|
62
|
+
modularity.
|
|
63
|
+
"""
|
|
64
|
+
|
|
56
65
|
hooks: RealtimeAgentHooks | None = None
|
|
57
66
|
"""A class that receives callbacks on various lifecycle events for this agent.
|
|
58
67
|
"""
|