openai-agents 0.3.2__py3-none-any.whl → 0.4.0__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/__init__.py +37 -1
- agents/_run_impl.py +252 -26
- agents/exceptions.py +35 -0
- agents/extensions/memory/__init__.py +21 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/redis_session.py +267 -0
- agents/extensions/memory/sqlalchemy_session.py +12 -3
- agents/extensions/models/litellm_model.py +123 -3
- agents/items.py +100 -4
- agents/mcp/server.py +43 -11
- agents/mcp/util.py +17 -1
- agents/memory/openai_conversations_session.py +2 -2
- agents/models/chatcmpl_converter.py +50 -20
- agents/models/openai_chatcompletions.py +27 -26
- agents/models/openai_responses.py +31 -29
- agents/realtime/handoffs.py +1 -1
- agents/result.py +55 -11
- agents/run.py +225 -27
- agents/strict_schema.py +14 -0
- agents/tool.py +80 -3
- agents/tool_guardrails.py +279 -0
- {openai_agents-0.3.2.dist-info → openai_agents-0.4.0.dist-info}/METADATA +14 -3
- {openai_agents-0.3.2.dist-info → openai_agents-0.4.0.dist-info}/RECORD +25 -22
- {openai_agents-0.3.2.dist-info → openai_agents-0.4.0.dist-info}/WHEEL +0 -0
- {openai_agents-0.3.2.dist-info → openai_agents-0.4.0.dist-info}/licenses/LICENSE +0 -0
agents/mcp/server.py
CHANGED
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar
|
|
|
11
11
|
|
|
12
12
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
13
13
|
from mcp import ClientSession, StdioServerParameters, Tool as MCPTool, stdio_client
|
|
14
|
+
from mcp.client.session import MessageHandlerFnT
|
|
14
15
|
from mcp.client.sse import sse_client
|
|
15
16
|
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
|
|
16
17
|
from mcp.shared.message import SessionMessage
|
|
@@ -20,7 +21,7 @@ from typing_extensions import NotRequired, TypedDict
|
|
|
20
21
|
from ..exceptions import UserError
|
|
21
22
|
from ..logger import logger
|
|
22
23
|
from ..run_context import RunContextWrapper
|
|
23
|
-
from .util import ToolFilter, ToolFilterContext, ToolFilterStatic
|
|
24
|
+
from .util import HttpClientFactory, ToolFilter, ToolFilterContext, ToolFilterStatic
|
|
24
25
|
|
|
25
26
|
T = TypeVar("T")
|
|
26
27
|
|
|
@@ -103,6 +104,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
103
104
|
use_structured_content: bool = False,
|
|
104
105
|
max_retry_attempts: int = 0,
|
|
105
106
|
retry_backoff_seconds_base: float = 1.0,
|
|
107
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
106
108
|
):
|
|
107
109
|
"""
|
|
108
110
|
Args:
|
|
@@ -124,6 +126,8 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
124
126
|
Defaults to no retries.
|
|
125
127
|
retry_backoff_seconds_base: The base delay, in seconds, used for exponential
|
|
126
128
|
backoff between retries.
|
|
129
|
+
message_handler: Optional handler invoked for session messages as delivered by the
|
|
130
|
+
ClientSession.
|
|
127
131
|
"""
|
|
128
132
|
super().__init__(use_structured_content=use_structured_content)
|
|
129
133
|
self.session: ClientSession | None = None
|
|
@@ -135,6 +139,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
135
139
|
self.client_session_timeout_seconds = client_session_timeout_seconds
|
|
136
140
|
self.max_retry_attempts = max_retry_attempts
|
|
137
141
|
self.retry_backoff_seconds_base = retry_backoff_seconds_base
|
|
142
|
+
self.message_handler = message_handler
|
|
138
143
|
|
|
139
144
|
# The cache is always dirty at startup, so that we fetch tools at least once
|
|
140
145
|
self._cache_dirty = True
|
|
@@ -272,6 +277,7 @@ class _MCPServerWithClientSession(MCPServer, abc.ABC):
|
|
|
272
277
|
timedelta(seconds=self.client_session_timeout_seconds)
|
|
273
278
|
if self.client_session_timeout_seconds
|
|
274
279
|
else None,
|
|
280
|
+
message_handler=self.message_handler,
|
|
275
281
|
)
|
|
276
282
|
)
|
|
277
283
|
server_result = await session.initialize()
|
|
@@ -394,6 +400,7 @@ class MCPServerStdio(_MCPServerWithClientSession):
|
|
|
394
400
|
use_structured_content: bool = False,
|
|
395
401
|
max_retry_attempts: int = 0,
|
|
396
402
|
retry_backoff_seconds_base: float = 1.0,
|
|
403
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
397
404
|
):
|
|
398
405
|
"""Create a new MCP server based on the stdio transport.
|
|
399
406
|
|
|
@@ -421,6 +428,8 @@ class MCPServerStdio(_MCPServerWithClientSession):
|
|
|
421
428
|
Defaults to no retries.
|
|
422
429
|
retry_backoff_seconds_base: The base delay, in seconds, for exponential
|
|
423
430
|
backoff between retries.
|
|
431
|
+
message_handler: Optional handler invoked for session messages as delivered by the
|
|
432
|
+
ClientSession.
|
|
424
433
|
"""
|
|
425
434
|
super().__init__(
|
|
426
435
|
cache_tools_list,
|
|
@@ -429,6 +438,7 @@ class MCPServerStdio(_MCPServerWithClientSession):
|
|
|
429
438
|
use_structured_content,
|
|
430
439
|
max_retry_attempts,
|
|
431
440
|
retry_backoff_seconds_base,
|
|
441
|
+
message_handler=message_handler,
|
|
432
442
|
)
|
|
433
443
|
|
|
434
444
|
self.params = StdioServerParameters(
|
|
@@ -492,6 +502,7 @@ class MCPServerSse(_MCPServerWithClientSession):
|
|
|
492
502
|
use_structured_content: bool = False,
|
|
493
503
|
max_retry_attempts: int = 0,
|
|
494
504
|
retry_backoff_seconds_base: float = 1.0,
|
|
505
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
495
506
|
):
|
|
496
507
|
"""Create a new MCP server based on the HTTP with SSE transport.
|
|
497
508
|
|
|
@@ -521,6 +532,8 @@ class MCPServerSse(_MCPServerWithClientSession):
|
|
|
521
532
|
Defaults to no retries.
|
|
522
533
|
retry_backoff_seconds_base: The base delay, in seconds, for exponential
|
|
523
534
|
backoff between retries.
|
|
535
|
+
message_handler: Optional handler invoked for session messages as delivered by the
|
|
536
|
+
ClientSession.
|
|
524
537
|
"""
|
|
525
538
|
super().__init__(
|
|
526
539
|
cache_tools_list,
|
|
@@ -529,6 +542,7 @@ class MCPServerSse(_MCPServerWithClientSession):
|
|
|
529
542
|
use_structured_content,
|
|
530
543
|
max_retry_attempts,
|
|
531
544
|
retry_backoff_seconds_base,
|
|
545
|
+
message_handler=message_handler,
|
|
532
546
|
)
|
|
533
547
|
|
|
534
548
|
self.params = params
|
|
@@ -575,6 +589,9 @@ class MCPServerStreamableHttpParams(TypedDict):
|
|
|
575
589
|
terminate_on_close: NotRequired[bool]
|
|
576
590
|
"""Terminate on close"""
|
|
577
591
|
|
|
592
|
+
httpx_client_factory: NotRequired[HttpClientFactory]
|
|
593
|
+
"""Custom HTTP client factory for configuring httpx.AsyncClient behavior."""
|
|
594
|
+
|
|
578
595
|
|
|
579
596
|
class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
580
597
|
"""MCP server implementation that uses the Streamable HTTP transport. See the [spec]
|
|
@@ -592,14 +609,15 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
592
609
|
use_structured_content: bool = False,
|
|
593
610
|
max_retry_attempts: int = 0,
|
|
594
611
|
retry_backoff_seconds_base: float = 1.0,
|
|
612
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
595
613
|
):
|
|
596
614
|
"""Create a new MCP server based on the Streamable HTTP transport.
|
|
597
615
|
|
|
598
616
|
Args:
|
|
599
617
|
params: The params that configure the server. This includes the URL of the server,
|
|
600
|
-
the headers to send to the server, the timeout for the HTTP request,
|
|
601
|
-
timeout for the Streamable HTTP connection
|
|
602
|
-
terminate on close.
|
|
618
|
+
the headers to send to the server, the timeout for the HTTP request, the
|
|
619
|
+
timeout for the Streamable HTTP connection, whether we need to
|
|
620
|
+
terminate on close, and an optional custom HTTP client factory.
|
|
603
621
|
|
|
604
622
|
cache_tools_list: Whether to cache the tools list. If `True`, the tools list will be
|
|
605
623
|
cached and only fetched from the server once. If `False`, the tools list will be
|
|
@@ -622,6 +640,8 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
622
640
|
Defaults to no retries.
|
|
623
641
|
retry_backoff_seconds_base: The base delay, in seconds, for exponential
|
|
624
642
|
backoff between retries.
|
|
643
|
+
message_handler: Optional handler invoked for session messages as delivered by the
|
|
644
|
+
ClientSession.
|
|
625
645
|
"""
|
|
626
646
|
super().__init__(
|
|
627
647
|
cache_tools_list,
|
|
@@ -630,6 +650,7 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
630
650
|
use_structured_content,
|
|
631
651
|
max_retry_attempts,
|
|
632
652
|
retry_backoff_seconds_base,
|
|
653
|
+
message_handler=message_handler,
|
|
633
654
|
)
|
|
634
655
|
|
|
635
656
|
self.params = params
|
|
@@ -645,13 +666,24 @@ class MCPServerStreamableHttp(_MCPServerWithClientSession):
|
|
|
645
666
|
]
|
|
646
667
|
]:
|
|
647
668
|
"""Create the streams for the server."""
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
669
|
+
# Only pass httpx_client_factory if it's provided
|
|
670
|
+
if "httpx_client_factory" in self.params:
|
|
671
|
+
return streamablehttp_client(
|
|
672
|
+
url=self.params["url"],
|
|
673
|
+
headers=self.params.get("headers", None),
|
|
674
|
+
timeout=self.params.get("timeout", 5),
|
|
675
|
+
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
|
|
676
|
+
terminate_on_close=self.params.get("terminate_on_close", True),
|
|
677
|
+
httpx_client_factory=self.params["httpx_client_factory"],
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
return streamablehttp_client(
|
|
681
|
+
url=self.params["url"],
|
|
682
|
+
headers=self.params.get("headers", None),
|
|
683
|
+
timeout=self.params.get("timeout", 5),
|
|
684
|
+
sse_read_timeout=self.params.get("sse_read_timeout", 60 * 5),
|
|
685
|
+
terminate_on_close=self.params.get("terminate_on_close", True),
|
|
686
|
+
)
|
|
655
687
|
|
|
656
688
|
@property
|
|
657
689
|
def name(self) -> str:
|
agents/mcp/util.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import json
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol, Union
|
|
5
5
|
|
|
6
|
+
import httpx
|
|
6
7
|
from typing_extensions import NotRequired, TypedDict
|
|
7
8
|
|
|
8
9
|
from .. import _debug
|
|
@@ -21,6 +22,21 @@ if TYPE_CHECKING:
|
|
|
21
22
|
from .server import MCPServer
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
class HttpClientFactory(Protocol):
|
|
26
|
+
"""Protocol for HTTP client factory functions.
|
|
27
|
+
|
|
28
|
+
This interface matches the MCP SDK's McpHttpClientFactory but is defined locally
|
|
29
|
+
to avoid accessing internal MCP SDK modules.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __call__(
|
|
33
|
+
self,
|
|
34
|
+
headers: Optional[dict[str, str]] = None,
|
|
35
|
+
timeout: Optional[httpx.Timeout] = None,
|
|
36
|
+
auth: Optional[httpx.Auth] = None,
|
|
37
|
+
) -> httpx.AsyncClient: ...
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
@dataclass
|
|
25
41
|
class ToolFilterContext:
|
|
26
42
|
"""Context information available to tool filter functions."""
|
|
@@ -50,7 +50,7 @@ class OpenAIConversationsSession(SessionABC):
|
|
|
50
50
|
order="asc",
|
|
51
51
|
):
|
|
52
52
|
# calling model_dump() to make this serializable
|
|
53
|
-
all_items.append(item.model_dump())
|
|
53
|
+
all_items.append(item.model_dump(exclude_unset=True))
|
|
54
54
|
else:
|
|
55
55
|
async for item in self._openai_client.conversations.items.list(
|
|
56
56
|
conversation_id=session_id,
|
|
@@ -58,7 +58,7 @@ class OpenAIConversationsSession(SessionABC):
|
|
|
58
58
|
order="desc",
|
|
59
59
|
):
|
|
60
60
|
# calling model_dump() to make this serializable
|
|
61
|
-
all_items.append(item.model_dump())
|
|
61
|
+
all_items.append(item.model_dump(exclude_unset=True))
|
|
62
62
|
if limit is not None and len(all_items) >= limit:
|
|
63
63
|
break
|
|
64
64
|
all_items.reverse()
|
|
@@ -2,12 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from collections.abc import Iterable
|
|
5
|
-
from typing import Any, Literal, cast
|
|
5
|
+
from typing import Any, Literal, Union, cast
|
|
6
6
|
|
|
7
|
-
from openai import
|
|
7
|
+
from openai import Omit, omit
|
|
8
8
|
from openai.types.chat import (
|
|
9
9
|
ChatCompletionAssistantMessageParam,
|
|
10
10
|
ChatCompletionContentPartImageParam,
|
|
11
|
+
ChatCompletionContentPartInputAudioParam,
|
|
11
12
|
ChatCompletionContentPartParam,
|
|
12
13
|
ChatCompletionContentPartTextParam,
|
|
13
14
|
ChatCompletionDeveloperMessageParam,
|
|
@@ -27,6 +28,7 @@ from openai.types.responses import (
|
|
|
27
28
|
ResponseFileSearchToolCallParam,
|
|
28
29
|
ResponseFunctionToolCall,
|
|
29
30
|
ResponseFunctionToolCallParam,
|
|
31
|
+
ResponseInputAudioParam,
|
|
30
32
|
ResponseInputContentParam,
|
|
31
33
|
ResponseInputFileParam,
|
|
32
34
|
ResponseInputImageParam,
|
|
@@ -54,9 +56,9 @@ class Converter:
|
|
|
54
56
|
@classmethod
|
|
55
57
|
def convert_tool_choice(
|
|
56
58
|
cls, tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None
|
|
57
|
-
) -> ChatCompletionToolChoiceOptionParam |
|
|
59
|
+
) -> ChatCompletionToolChoiceOptionParam | Omit:
|
|
58
60
|
if tool_choice is None:
|
|
59
|
-
return
|
|
61
|
+
return omit
|
|
60
62
|
elif isinstance(tool_choice, MCPToolChoice):
|
|
61
63
|
raise UserError("MCPToolChoice is not supported for Chat Completions models")
|
|
62
64
|
elif tool_choice == "auto":
|
|
@@ -76,9 +78,9 @@ class Converter:
|
|
|
76
78
|
@classmethod
|
|
77
79
|
def convert_response_format(
|
|
78
80
|
cls, final_output_schema: AgentOutputSchemaBase | None
|
|
79
|
-
) -> ResponseFormat |
|
|
81
|
+
) -> ResponseFormat | Omit:
|
|
80
82
|
if not final_output_schema or final_output_schema.is_plain_text():
|
|
81
|
-
return
|
|
83
|
+
return omit
|
|
82
84
|
|
|
83
85
|
return {
|
|
84
86
|
"type": "json_schema",
|
|
@@ -287,23 +289,44 @@ class Converter:
|
|
|
287
289
|
},
|
|
288
290
|
)
|
|
289
291
|
)
|
|
292
|
+
elif isinstance(c, dict) and c.get("type") == "input_audio":
|
|
293
|
+
casted_audio_param = cast(ResponseInputAudioParam, c)
|
|
294
|
+
audio_payload = casted_audio_param.get("input_audio")
|
|
295
|
+
if not audio_payload:
|
|
296
|
+
raise UserError(
|
|
297
|
+
f"Only audio data is supported for input_audio {casted_audio_param}"
|
|
298
|
+
)
|
|
299
|
+
if not isinstance(audio_payload, dict):
|
|
300
|
+
raise UserError(
|
|
301
|
+
f"input_audio must provide audio data and format {casted_audio_param}"
|
|
302
|
+
)
|
|
303
|
+
audio_data = audio_payload.get("data")
|
|
304
|
+
audio_format = audio_payload.get("format")
|
|
305
|
+
if not audio_data or not audio_format:
|
|
306
|
+
raise UserError(
|
|
307
|
+
f"input_audio requires both data and format {casted_audio_param}"
|
|
308
|
+
)
|
|
309
|
+
out.append(
|
|
310
|
+
ChatCompletionContentPartInputAudioParam(
|
|
311
|
+
type="input_audio",
|
|
312
|
+
input_audio={
|
|
313
|
+
"data": audio_data,
|
|
314
|
+
"format": audio_format,
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
)
|
|
290
318
|
elif isinstance(c, dict) and c.get("type") == "input_file":
|
|
291
319
|
casted_file_param = cast(ResponseInputFileParam, c)
|
|
292
320
|
if "file_data" not in casted_file_param or not casted_file_param["file_data"]:
|
|
293
321
|
raise UserError(
|
|
294
322
|
f"Only file_data is supported for input_file {casted_file_param}"
|
|
295
323
|
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
file_data=casted_file_param["file_data"],
|
|
303
|
-
filename=casted_file_param["filename"],
|
|
304
|
-
),
|
|
305
|
-
)
|
|
306
|
-
)
|
|
324
|
+
filedata = FileFile(file_data=casted_file_param["file_data"])
|
|
325
|
+
|
|
326
|
+
if "filename" in casted_file_param and casted_file_param["filename"]:
|
|
327
|
+
filedata["filename"] = casted_file_param["filename"]
|
|
328
|
+
|
|
329
|
+
out.append(File(type="file", file=filedata))
|
|
307
330
|
else:
|
|
308
331
|
raise UserError(f"Unknown content: {c}")
|
|
309
332
|
return out
|
|
@@ -511,10 +534,13 @@ class Converter:
|
|
|
511
534
|
# 5) function call output => tool message
|
|
512
535
|
elif func_output := cls.maybe_function_tool_call_output(item):
|
|
513
536
|
flush_assistant_message()
|
|
537
|
+
output_content = cast(
|
|
538
|
+
Union[str, Iterable[ResponseInputContentParam]], func_output["output"]
|
|
539
|
+
)
|
|
514
540
|
msg: ChatCompletionToolMessageParam = {
|
|
515
541
|
"role": "tool",
|
|
516
542
|
"tool_call_id": func_output["call_id"],
|
|
517
|
-
"content":
|
|
543
|
+
"content": cls.extract_text_content(output_content),
|
|
518
544
|
}
|
|
519
545
|
result.append(msg)
|
|
520
546
|
|
|
@@ -533,7 +559,7 @@ class Converter:
|
|
|
533
559
|
|
|
534
560
|
if content_items and preserve_thinking_blocks:
|
|
535
561
|
# Reconstruct thinking blocks from content and signature
|
|
536
|
-
|
|
562
|
+
reconstructed_thinking_blocks = []
|
|
537
563
|
for content_item in content_items:
|
|
538
564
|
if (
|
|
539
565
|
isinstance(content_item, dict)
|
|
@@ -546,7 +572,11 @@ class Converter:
|
|
|
546
572
|
# Add signatures if available
|
|
547
573
|
if signatures:
|
|
548
574
|
thinking_block["signature"] = signatures.pop(0)
|
|
549
|
-
|
|
575
|
+
reconstructed_thinking_blocks.append(thinking_block)
|
|
576
|
+
|
|
577
|
+
# Store thinking blocks as pending for the next assistant message
|
|
578
|
+
# This preserves the original behavior
|
|
579
|
+
pending_thinking_blocks = reconstructed_thinking_blocks
|
|
550
580
|
|
|
551
581
|
# 8) If we haven't recognized it => fail or ignore
|
|
552
582
|
else:
|
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import time
|
|
5
5
|
from collections.abc import AsyncIterator
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
|
7
7
|
|
|
8
|
-
from openai import
|
|
8
|
+
from openai import AsyncOpenAI, AsyncStream, Omit, omit
|
|
9
9
|
from openai.types import ChatModel
|
|
10
10
|
from openai.types.chat import ChatCompletion, ChatCompletionChunk, ChatCompletionMessage
|
|
11
11
|
from openai.types.chat.chat_completion import Choice
|
|
@@ -44,8 +44,8 @@ class OpenAIChatCompletionsModel(Model):
|
|
|
44
44
|
self.model = model
|
|
45
45
|
self._client = openai_client
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
return value if value is not None else
|
|
47
|
+
def _non_null_or_omit(self, value: Any) -> Any:
|
|
48
|
+
return value if value is not None else omit
|
|
49
49
|
|
|
50
50
|
async def get_response(
|
|
51
51
|
self,
|
|
@@ -243,13 +243,12 @@ class OpenAIChatCompletionsModel(Model):
|
|
|
243
243
|
if tracing.include_data():
|
|
244
244
|
span.span_data.input = converted_messages
|
|
245
245
|
|
|
246
|
-
parallel_tool_calls
|
|
247
|
-
True
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
246
|
+
if model_settings.parallel_tool_calls and tools:
|
|
247
|
+
parallel_tool_calls: bool | Omit = True
|
|
248
|
+
elif model_settings.parallel_tool_calls is False:
|
|
249
|
+
parallel_tool_calls = False
|
|
250
|
+
else:
|
|
251
|
+
parallel_tool_calls = omit
|
|
253
252
|
tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
|
|
254
253
|
response_format = Converter.convert_response_format(output_schema)
|
|
255
254
|
|
|
@@ -259,6 +258,7 @@ class OpenAIChatCompletionsModel(Model):
|
|
|
259
258
|
converted_tools.append(Converter.convert_handoff_tool(handoff))
|
|
260
259
|
|
|
261
260
|
converted_tools = _to_dump_compatible(converted_tools)
|
|
261
|
+
tools_param = converted_tools if converted_tools else omit
|
|
262
262
|
|
|
263
263
|
if _debug.DONT_LOG_MODEL_DATA:
|
|
264
264
|
logger.debug("Calling LLM")
|
|
@@ -288,28 +288,30 @@ class OpenAIChatCompletionsModel(Model):
|
|
|
288
288
|
self._get_client(), model_settings, stream=stream
|
|
289
289
|
)
|
|
290
290
|
|
|
291
|
+
stream_param: Literal[True] | Omit = True if stream else omit
|
|
292
|
+
|
|
291
293
|
ret = await self._get_client().chat.completions.create(
|
|
292
294
|
model=self.model,
|
|
293
295
|
messages=converted_messages,
|
|
294
|
-
tools=
|
|
295
|
-
temperature=self.
|
|
296
|
-
top_p=self.
|
|
297
|
-
frequency_penalty=self.
|
|
298
|
-
presence_penalty=self.
|
|
299
|
-
max_tokens=self.
|
|
296
|
+
tools=tools_param,
|
|
297
|
+
temperature=self._non_null_or_omit(model_settings.temperature),
|
|
298
|
+
top_p=self._non_null_or_omit(model_settings.top_p),
|
|
299
|
+
frequency_penalty=self._non_null_or_omit(model_settings.frequency_penalty),
|
|
300
|
+
presence_penalty=self._non_null_or_omit(model_settings.presence_penalty),
|
|
301
|
+
max_tokens=self._non_null_or_omit(model_settings.max_tokens),
|
|
300
302
|
tool_choice=tool_choice,
|
|
301
303
|
response_format=response_format,
|
|
302
304
|
parallel_tool_calls=parallel_tool_calls,
|
|
303
|
-
stream=
|
|
304
|
-
stream_options=self.
|
|
305
|
-
store=self.
|
|
306
|
-
reasoning_effort=self.
|
|
307
|
-
verbosity=self.
|
|
308
|
-
top_logprobs=self.
|
|
305
|
+
stream=cast(Any, stream_param),
|
|
306
|
+
stream_options=self._non_null_or_omit(stream_options),
|
|
307
|
+
store=self._non_null_or_omit(store),
|
|
308
|
+
reasoning_effort=self._non_null_or_omit(reasoning_effort),
|
|
309
|
+
verbosity=self._non_null_or_omit(model_settings.verbosity),
|
|
310
|
+
top_logprobs=self._non_null_or_omit(model_settings.top_logprobs),
|
|
309
311
|
extra_headers=self._merge_headers(model_settings),
|
|
310
312
|
extra_query=model_settings.extra_query,
|
|
311
313
|
extra_body=model_settings.extra_body,
|
|
312
|
-
metadata=self.
|
|
314
|
+
metadata=self._non_null_or_omit(model_settings.metadata),
|
|
313
315
|
**(model_settings.extra_args or {}),
|
|
314
316
|
)
|
|
315
317
|
|
|
@@ -319,14 +321,13 @@ class OpenAIChatCompletionsModel(Model):
|
|
|
319
321
|
responses_tool_choice = OpenAIResponsesConverter.convert_tool_choice(
|
|
320
322
|
model_settings.tool_choice
|
|
321
323
|
)
|
|
322
|
-
if responses_tool_choice is None or responses_tool_choice
|
|
324
|
+
if responses_tool_choice is None or responses_tool_choice is omit:
|
|
323
325
|
# For Responses API data compatibility with Chat Completions patterns,
|
|
324
326
|
# we need to set "none" if tool_choice is absent.
|
|
325
327
|
# Without this fix, you'll get the following error:
|
|
326
328
|
# pydantic_core._pydantic_core.ValidationError: 4 validation errors for Response
|
|
327
329
|
# tool_choice.literal['none','auto','required']
|
|
328
330
|
# Input should be 'none', 'auto' or 'required'
|
|
329
|
-
# [type=literal_error, input_value=NOT_GIVEN, input_type=NotGiven]
|
|
330
331
|
# see also: https://github.com/openai/openai-agents-python/issues/980
|
|
331
332
|
responses_tool_choice = "auto"
|
|
332
333
|
|
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
5
|
from contextvars import ContextVar
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, Union, cast, overload
|
|
8
8
|
|
|
9
|
-
from openai import
|
|
9
|
+
from openai import APIStatusError, AsyncOpenAI, AsyncStream, Omit, omit
|
|
10
10
|
from openai.types import ChatModel
|
|
11
11
|
from openai.types.responses import (
|
|
12
12
|
Response,
|
|
@@ -69,8 +69,8 @@ class OpenAIResponsesModel(Model):
|
|
|
69
69
|
self.model = model
|
|
70
70
|
self._client = openai_client
|
|
71
71
|
|
|
72
|
-
def
|
|
73
|
-
return value if value is not None else
|
|
72
|
+
def _non_null_or_omit(self, value: Any) -> Any:
|
|
73
|
+
return value if value is not None else omit
|
|
74
74
|
|
|
75
75
|
async def get_response(
|
|
76
76
|
self,
|
|
@@ -249,13 +249,12 @@ class OpenAIResponsesModel(Model):
|
|
|
249
249
|
list_input = ItemHelpers.input_to_new_input_list(input)
|
|
250
250
|
list_input = _to_dump_compatible(list_input)
|
|
251
251
|
|
|
252
|
-
parallel_tool_calls
|
|
253
|
-
True
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
252
|
+
if model_settings.parallel_tool_calls and tools:
|
|
253
|
+
parallel_tool_calls: bool | Omit = True
|
|
254
|
+
elif model_settings.parallel_tool_calls is False:
|
|
255
|
+
parallel_tool_calls = False
|
|
256
|
+
else:
|
|
257
|
+
parallel_tool_calls = omit
|
|
259
258
|
|
|
260
259
|
tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
|
|
261
260
|
converted_tools = Converter.convert_tools(tools, handoffs)
|
|
@@ -297,36 +296,39 @@ class OpenAIResponsesModel(Model):
|
|
|
297
296
|
if model_settings.top_logprobs is not None:
|
|
298
297
|
extra_args["top_logprobs"] = model_settings.top_logprobs
|
|
299
298
|
if model_settings.verbosity is not None:
|
|
300
|
-
if response_format
|
|
299
|
+
if response_format is not omit:
|
|
301
300
|
response_format["verbosity"] = model_settings.verbosity # type: ignore [index]
|
|
302
301
|
else:
|
|
303
302
|
response_format = {"verbosity": model_settings.verbosity}
|
|
304
303
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
304
|
+
stream_param: Literal[True] | Omit = True if stream else omit
|
|
305
|
+
|
|
306
|
+
response = await self._client.responses.create(
|
|
307
|
+
previous_response_id=self._non_null_or_omit(previous_response_id),
|
|
308
|
+
conversation=self._non_null_or_omit(conversation_id),
|
|
309
|
+
instructions=self._non_null_or_omit(system_instructions),
|
|
309
310
|
model=self.model,
|
|
310
311
|
input=list_input,
|
|
311
312
|
include=include,
|
|
312
313
|
tools=converted_tools_payload,
|
|
313
|
-
prompt=self.
|
|
314
|
-
temperature=self.
|
|
315
|
-
top_p=self.
|
|
316
|
-
truncation=self.
|
|
317
|
-
max_output_tokens=self.
|
|
314
|
+
prompt=self._non_null_or_omit(prompt),
|
|
315
|
+
temperature=self._non_null_or_omit(model_settings.temperature),
|
|
316
|
+
top_p=self._non_null_or_omit(model_settings.top_p),
|
|
317
|
+
truncation=self._non_null_or_omit(model_settings.truncation),
|
|
318
|
+
max_output_tokens=self._non_null_or_omit(model_settings.max_tokens),
|
|
318
319
|
tool_choice=tool_choice,
|
|
319
320
|
parallel_tool_calls=parallel_tool_calls,
|
|
320
|
-
stream=
|
|
321
|
+
stream=cast(Any, stream_param),
|
|
321
322
|
extra_headers=self._merge_headers(model_settings),
|
|
322
323
|
extra_query=model_settings.extra_query,
|
|
323
324
|
extra_body=model_settings.extra_body,
|
|
324
325
|
text=response_format,
|
|
325
|
-
store=self.
|
|
326
|
-
reasoning=self.
|
|
327
|
-
metadata=self.
|
|
326
|
+
store=self._non_null_or_omit(model_settings.store),
|
|
327
|
+
reasoning=self._non_null_or_omit(model_settings.reasoning),
|
|
328
|
+
metadata=self._non_null_or_omit(model_settings.metadata),
|
|
328
329
|
**extra_args,
|
|
329
330
|
)
|
|
331
|
+
return cast(Union[Response, AsyncStream[ResponseStreamEvent]], response)
|
|
330
332
|
|
|
331
333
|
def _get_client(self) -> AsyncOpenAI:
|
|
332
334
|
if self._client is None:
|
|
@@ -351,9 +353,9 @@ class Converter:
|
|
|
351
353
|
@classmethod
|
|
352
354
|
def convert_tool_choice(
|
|
353
355
|
cls, tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None
|
|
354
|
-
) -> response_create_params.ToolChoice |
|
|
356
|
+
) -> response_create_params.ToolChoice | Omit:
|
|
355
357
|
if tool_choice is None:
|
|
356
|
-
return
|
|
358
|
+
return omit
|
|
357
359
|
elif isinstance(tool_choice, MCPToolChoice):
|
|
358
360
|
return {
|
|
359
361
|
"server_label": tool_choice.server_label,
|
|
@@ -404,9 +406,9 @@ class Converter:
|
|
|
404
406
|
@classmethod
|
|
405
407
|
def get_response_format(
|
|
406
408
|
cls, output_schema: AgentOutputSchemaBase | None
|
|
407
|
-
) -> ResponseTextConfigParam |
|
|
409
|
+
) -> ResponseTextConfigParam | Omit:
|
|
408
410
|
if output_schema is None or output_schema.is_plain_text():
|
|
409
|
-
return
|
|
411
|
+
return omit
|
|
410
412
|
else:
|
|
411
413
|
return {
|
|
412
414
|
"format": {
|
agents/realtime/handoffs.py
CHANGED
|
@@ -13,10 +13,10 @@ from ..strict_schema import ensure_strict_json_schema
|
|
|
13
13
|
from ..tracing.spans import SpanError
|
|
14
14
|
from ..util import _error_tracing, _json
|
|
15
15
|
from ..util._types import MaybeAwaitable
|
|
16
|
+
from . import RealtimeAgent
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from ..agent import AgentBase
|
|
19
|
-
from . import RealtimeAgent
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
# The handoff input type is the type of data passed when the agent is called via a handoff.
|