openai-agents 0.3.3__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 +12 -0
- agents/_run_impl.py +16 -6
- agents/extensions/memory/__init__.py +1 -3
- agents/extensions/memory/sqlalchemy_session.py +12 -3
- agents/extensions/models/litellm_model.py +3 -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 +44 -18
- agents/models/openai_chatcompletions.py +27 -26
- agents/models/openai_responses.py +31 -29
- agents/realtime/handoffs.py +1 -1
- agents/result.py +48 -11
- agents/run.py +214 -27
- agents/strict_schema.py +14 -0
- agents/tool.py +72 -3
- {openai_agents-0.3.3.dist-info → openai_agents-0.4.0.dist-info}/METADATA +2 -2
- {openai_agents-0.3.3.dist-info → openai_agents-0.4.0.dist-info}/RECORD +21 -21
- {openai_agents-0.3.3.dist-info → openai_agents-0.4.0.dist-info}/WHEEL +0 -0
- {openai_agents-0.3.3.dist-info → openai_agents-0.4.0.dist-info}/licenses/LICENSE +0 -0
agents/__init__.py
CHANGED
|
@@ -81,6 +81,12 @@ from .tool import (
|
|
|
81
81
|
MCPToolApprovalFunctionResult,
|
|
82
82
|
MCPToolApprovalRequest,
|
|
83
83
|
Tool,
|
|
84
|
+
ToolOutputFileContent,
|
|
85
|
+
ToolOutputFileContentDict,
|
|
86
|
+
ToolOutputImage,
|
|
87
|
+
ToolOutputImageDict,
|
|
88
|
+
ToolOutputText,
|
|
89
|
+
ToolOutputTextDict,
|
|
84
90
|
WebSearchTool,
|
|
85
91
|
default_tool_error_function,
|
|
86
92
|
function_tool,
|
|
@@ -273,6 +279,12 @@ __all__ = [
|
|
|
273
279
|
"MCPToolApprovalFunction",
|
|
274
280
|
"MCPToolApprovalRequest",
|
|
275
281
|
"MCPToolApprovalFunctionResult",
|
|
282
|
+
"ToolOutputText",
|
|
283
|
+
"ToolOutputTextDict",
|
|
284
|
+
"ToolOutputImage",
|
|
285
|
+
"ToolOutputImageDict",
|
|
286
|
+
"ToolOutputFileContent",
|
|
287
|
+
"ToolOutputFileContentDict",
|
|
276
288
|
"function_tool",
|
|
277
289
|
"Usage",
|
|
278
290
|
"add_trace_processor",
|
agents/_run_impl.py
CHANGED
|
@@ -267,10 +267,11 @@ class RunImpl:
|
|
|
267
267
|
new_step_items: list[RunItem] = []
|
|
268
268
|
new_step_items.extend(processed_response.new_items)
|
|
269
269
|
|
|
270
|
-
# First, lets run the tool calls - function tools and
|
|
270
|
+
# First, lets run the tool calls - function tools, computer actions, and local shell calls
|
|
271
271
|
(
|
|
272
272
|
(function_results, tool_input_guardrail_results, tool_output_guardrail_results),
|
|
273
273
|
computer_results,
|
|
274
|
+
local_shell_results,
|
|
274
275
|
) = await asyncio.gather(
|
|
275
276
|
cls.execute_function_tool_calls(
|
|
276
277
|
agent=agent,
|
|
@@ -286,9 +287,17 @@ class RunImpl:
|
|
|
286
287
|
context_wrapper=context_wrapper,
|
|
287
288
|
config=run_config,
|
|
288
289
|
),
|
|
290
|
+
cls.execute_local_shell_calls(
|
|
291
|
+
agent=agent,
|
|
292
|
+
calls=processed_response.local_shell_calls,
|
|
293
|
+
hooks=hooks,
|
|
294
|
+
context_wrapper=context_wrapper,
|
|
295
|
+
config=run_config,
|
|
296
|
+
),
|
|
289
297
|
)
|
|
290
298
|
new_step_items.extend([result.run_item for result in function_results])
|
|
291
299
|
new_step_items.extend(computer_results)
|
|
300
|
+
new_step_items.extend(local_shell_results)
|
|
292
301
|
|
|
293
302
|
# Next, run the MCP approval requests
|
|
294
303
|
if processed_response.mcp_approval_requests:
|
|
@@ -823,7 +832,7 @@ class RunImpl:
|
|
|
823
832
|
output=result,
|
|
824
833
|
run_item=ToolCallOutputItem(
|
|
825
834
|
output=result,
|
|
826
|
-
raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call,
|
|
835
|
+
raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, result),
|
|
827
836
|
agent=agent,
|
|
828
837
|
),
|
|
829
838
|
)
|
|
@@ -1414,12 +1423,13 @@ class LocalShellAction:
|
|
|
1414
1423
|
|
|
1415
1424
|
return ToolCallOutputItem(
|
|
1416
1425
|
agent=agent,
|
|
1417
|
-
output=
|
|
1418
|
-
|
|
1426
|
+
output=result,
|
|
1427
|
+
# LocalShellCallOutput type uses the field name "id", but the server wants "call_id".
|
|
1428
|
+
# raw_item keeps the upstream type, so we ignore the type checker here.
|
|
1429
|
+
raw_item={ # type: ignore[misc, arg-type]
|
|
1419
1430
|
"type": "local_shell_call_output",
|
|
1420
|
-
"
|
|
1431
|
+
"call_id": call.tool_call.call_id,
|
|
1421
1432
|
"output": result,
|
|
1422
|
-
# "id": "out" + call.tool_call.id, # TODO remove this, it should be optional
|
|
1423
1433
|
},
|
|
1424
1434
|
)
|
|
1425
1435
|
|
|
@@ -58,8 +58,6 @@ def __getattr__(name: str) -> Any:
|
|
|
58
58
|
|
|
59
59
|
return AdvancedSQLiteSession
|
|
60
60
|
except ModuleNotFoundError as e:
|
|
61
|
-
raise ImportError(
|
|
62
|
-
f"Failed to import AdvancedSQLiteSession: {e}"
|
|
63
|
-
) from e
|
|
61
|
+
raise ImportError(f"Failed to import AdvancedSQLiteSession: {e}") from e
|
|
64
62
|
|
|
65
63
|
raise AttributeError(f"module {__name__} has no attribute {name}")
|
|
@@ -195,7 +195,10 @@ class SQLAlchemySession(SessionABC):
|
|
|
195
195
|
stmt = (
|
|
196
196
|
select(self._messages.c.message_data)
|
|
197
197
|
.where(self._messages.c.session_id == self.session_id)
|
|
198
|
-
.order_by(
|
|
198
|
+
.order_by(
|
|
199
|
+
self._messages.c.created_at.asc(),
|
|
200
|
+
self._messages.c.id.asc(),
|
|
201
|
+
)
|
|
199
202
|
)
|
|
200
203
|
else:
|
|
201
204
|
stmt = (
|
|
@@ -203,7 +206,10 @@ class SQLAlchemySession(SessionABC):
|
|
|
203
206
|
.where(self._messages.c.session_id == self.session_id)
|
|
204
207
|
# Use DESC + LIMIT to get the latest N
|
|
205
208
|
# then reverse later for chronological order.
|
|
206
|
-
.order_by(
|
|
209
|
+
.order_by(
|
|
210
|
+
self._messages.c.created_at.desc(),
|
|
211
|
+
self._messages.c.id.desc(),
|
|
212
|
+
)
|
|
207
213
|
.limit(limit)
|
|
208
214
|
)
|
|
209
215
|
|
|
@@ -278,7 +284,10 @@ class SQLAlchemySession(SessionABC):
|
|
|
278
284
|
subq = (
|
|
279
285
|
select(self._messages.c.id)
|
|
280
286
|
.where(self._messages.c.session_id == self.session_id)
|
|
281
|
-
.order_by(
|
|
287
|
+
.order_by(
|
|
288
|
+
self._messages.c.created_at.desc(),
|
|
289
|
+
self._messages.c.id.desc(),
|
|
290
|
+
)
|
|
282
291
|
.limit(1)
|
|
283
292
|
)
|
|
284
293
|
res = await sess.execute(subq)
|
|
@@ -18,7 +18,7 @@ except ImportError as _e:
|
|
|
18
18
|
"dependency group: `pip install 'openai-agents[litellm]'`."
|
|
19
19
|
) from _e
|
|
20
20
|
|
|
21
|
-
from openai import
|
|
21
|
+
from openai import AsyncStream, NotGiven, omit
|
|
22
22
|
from openai.types.chat import (
|
|
23
23
|
ChatCompletionChunk,
|
|
24
24
|
ChatCompletionMessageCustomToolCall,
|
|
@@ -374,7 +374,7 @@ class LitellmModel(Model):
|
|
|
374
374
|
object="response",
|
|
375
375
|
output=[],
|
|
376
376
|
tool_choice=cast(Literal["auto", "required", "none"], tool_choice)
|
|
377
|
-
if tool_choice
|
|
377
|
+
if tool_choice is not omit
|
|
378
378
|
else "auto",
|
|
379
379
|
top_p=model_settings.top_p,
|
|
380
380
|
temperature=model_settings.temperature,
|
|
@@ -500,7 +500,7 @@ class LitellmModel(Model):
|
|
|
500
500
|
return fixed_messages
|
|
501
501
|
|
|
502
502
|
def _remove_not_given(self, value: Any) -> Any:
|
|
503
|
-
if isinstance(value, NotGiven):
|
|
503
|
+
if value is omit or isinstance(value, NotGiven):
|
|
504
504
|
return None
|
|
505
505
|
return value
|
|
506
506
|
|
agents/items.py
CHANGED
|
@@ -21,6 +21,12 @@ from openai.types.responses import (
|
|
|
21
21
|
from openai.types.responses.response_code_interpreter_tool_call import (
|
|
22
22
|
ResponseCodeInterpreterToolCall,
|
|
23
23
|
)
|
|
24
|
+
from openai.types.responses.response_function_call_output_item_list_param import (
|
|
25
|
+
ResponseFunctionCallOutputItemListParam,
|
|
26
|
+
ResponseFunctionCallOutputItemParam,
|
|
27
|
+
)
|
|
28
|
+
from openai.types.responses.response_input_file_content_param import ResponseInputFileContentParam
|
|
29
|
+
from openai.types.responses.response_input_image_content_param import ResponseInputImageContentParam
|
|
24
30
|
from openai.types.responses.response_input_item_param import (
|
|
25
31
|
ComputerCallOutput,
|
|
26
32
|
FunctionCallOutput,
|
|
@@ -36,9 +42,17 @@ from openai.types.responses.response_output_item import (
|
|
|
36
42
|
)
|
|
37
43
|
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
38
44
|
from pydantic import BaseModel
|
|
39
|
-
from typing_extensions import TypeAlias
|
|
45
|
+
from typing_extensions import TypeAlias, assert_never
|
|
40
46
|
|
|
41
47
|
from .exceptions import AgentsException, ModelBehaviorError
|
|
48
|
+
from .logger import logger
|
|
49
|
+
from .tool import (
|
|
50
|
+
ToolOutputFileContent,
|
|
51
|
+
ToolOutputImage,
|
|
52
|
+
ToolOutputText,
|
|
53
|
+
ValidToolOutputPydanticModels,
|
|
54
|
+
ValidToolOutputPydanticModelsTypeAdapter,
|
|
55
|
+
)
|
|
42
56
|
from .usage import Usage
|
|
43
57
|
|
|
44
58
|
if TYPE_CHECKING:
|
|
@@ -298,11 +312,93 @@ class ItemHelpers:
|
|
|
298
312
|
|
|
299
313
|
@classmethod
|
|
300
314
|
def tool_call_output_item(
|
|
301
|
-
cls, tool_call: ResponseFunctionToolCall, output:
|
|
315
|
+
cls, tool_call: ResponseFunctionToolCall, output: Any
|
|
302
316
|
) -> FunctionCallOutput:
|
|
303
|
-
"""Creates a tool call output item from a tool call and its output.
|
|
317
|
+
"""Creates a tool call output item from a tool call and its output.
|
|
318
|
+
|
|
319
|
+
Accepts either plain values (stringified) or structured outputs using
|
|
320
|
+
input_text/input_image/input_file shapes. Structured outputs may be
|
|
321
|
+
provided as Pydantic models or dicts, or an iterable of such items.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
converted_output = cls._convert_tool_output(output)
|
|
325
|
+
|
|
304
326
|
return {
|
|
305
327
|
"call_id": tool_call.call_id,
|
|
306
|
-
"output":
|
|
328
|
+
"output": converted_output,
|
|
307
329
|
"type": "function_call_output",
|
|
308
330
|
}
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def _convert_tool_output(cls, output: Any) -> str | ResponseFunctionCallOutputItemListParam:
|
|
334
|
+
"""Converts a tool return value into an output acceptable by the Responses API."""
|
|
335
|
+
|
|
336
|
+
# If the output is either a single or list of the known structured output types, convert to
|
|
337
|
+
# ResponseFunctionCallOutputItemListParam. Else, just stringify.
|
|
338
|
+
if isinstance(output, (list, tuple)):
|
|
339
|
+
maybe_converted_output_list = [
|
|
340
|
+
cls._maybe_get_output_as_structured_function_output(item) for item in output
|
|
341
|
+
]
|
|
342
|
+
if all(maybe_converted_output_list):
|
|
343
|
+
return [
|
|
344
|
+
cls._convert_single_tool_output_pydantic_model(item)
|
|
345
|
+
for item in maybe_converted_output_list
|
|
346
|
+
if item is not None
|
|
347
|
+
]
|
|
348
|
+
else:
|
|
349
|
+
return str(output)
|
|
350
|
+
else:
|
|
351
|
+
maybe_converted_output = cls._maybe_get_output_as_structured_function_output(output)
|
|
352
|
+
if maybe_converted_output:
|
|
353
|
+
return [cls._convert_single_tool_output_pydantic_model(maybe_converted_output)]
|
|
354
|
+
else:
|
|
355
|
+
return str(output)
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def _maybe_get_output_as_structured_function_output(
|
|
359
|
+
cls, output: Any
|
|
360
|
+
) -> ValidToolOutputPydanticModels | None:
|
|
361
|
+
if isinstance(output, (ToolOutputText, ToolOutputImage, ToolOutputFileContent)):
|
|
362
|
+
return output
|
|
363
|
+
elif isinstance(output, dict):
|
|
364
|
+
try:
|
|
365
|
+
return ValidToolOutputPydanticModelsTypeAdapter.validate_python(output)
|
|
366
|
+
except pydantic.ValidationError:
|
|
367
|
+
logger.debug("dict was not a valid tool output pydantic model")
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def _convert_single_tool_output_pydantic_model(
|
|
374
|
+
cls, output: ValidToolOutputPydanticModels
|
|
375
|
+
) -> ResponseFunctionCallOutputItemParam:
|
|
376
|
+
if isinstance(output, ToolOutputText):
|
|
377
|
+
return {"type": "input_text", "text": output.text}
|
|
378
|
+
elif isinstance(output, ToolOutputImage):
|
|
379
|
+
# Forward all provided optional fields so the Responses API receives
|
|
380
|
+
# the correct identifiers and settings for the image resource.
|
|
381
|
+
result: ResponseInputImageContentParam = {"type": "input_image"}
|
|
382
|
+
if output.image_url is not None:
|
|
383
|
+
result["image_url"] = output.image_url
|
|
384
|
+
if output.file_id is not None:
|
|
385
|
+
result["file_id"] = output.file_id
|
|
386
|
+
if output.detail is not None:
|
|
387
|
+
result["detail"] = output.detail
|
|
388
|
+
return result
|
|
389
|
+
elif isinstance(output, ToolOutputFileContent):
|
|
390
|
+
# Forward all provided optional fields so the Responses API receives
|
|
391
|
+
# the correct identifiers and metadata for the file resource.
|
|
392
|
+
result_file: ResponseInputFileContentParam = {"type": "input_file"}
|
|
393
|
+
if output.file_data is not None:
|
|
394
|
+
result_file["file_data"] = output.file_data
|
|
395
|
+
if output.file_url is not None:
|
|
396
|
+
result_file["file_url"] = output.file_url
|
|
397
|
+
if output.file_id is not None:
|
|
398
|
+
result_file["file_id"] = output.file_id
|
|
399
|
+
if output.filename is not None:
|
|
400
|
+
result_file["filename"] = output.filename
|
|
401
|
+
return result_file
|
|
402
|
+
else:
|
|
403
|
+
assert_never(output)
|
|
404
|
+
raise ValueError(f"Unexpected tool output type: {output}")
|
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
|
|