openai-agents 0.2.8__py3-none-any.whl → 0.6.8__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.
- agents/__init__.py +105 -4
- agents/_debug.py +15 -4
- agents/_run_impl.py +1203 -96
- agents/agent.py +164 -19
- agents/apply_diff.py +329 -0
- agents/editor.py +47 -0
- agents/exceptions.py +35 -0
- agents/extensions/experimental/__init__.py +6 -0
- agents/extensions/experimental/codex/__init__.py +92 -0
- agents/extensions/experimental/codex/codex.py +89 -0
- agents/extensions/experimental/codex/codex_options.py +35 -0
- agents/extensions/experimental/codex/codex_tool.py +1142 -0
- agents/extensions/experimental/codex/events.py +162 -0
- agents/extensions/experimental/codex/exec.py +263 -0
- agents/extensions/experimental/codex/items.py +245 -0
- agents/extensions/experimental/codex/output_schema_file.py +50 -0
- agents/extensions/experimental/codex/payloads.py +31 -0
- agents/extensions/experimental/codex/thread.py +214 -0
- agents/extensions/experimental/codex/thread_options.py +54 -0
- agents/extensions/experimental/codex/turn_options.py +36 -0
- agents/extensions/handoff_filters.py +13 -1
- agents/extensions/memory/__init__.py +120 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/async_sqlite_session.py +239 -0
- agents/extensions/memory/dapr_session.py +423 -0
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/memory/redis_session.py +261 -0
- agents/extensions/memory/sqlalchemy_session.py +334 -0
- agents/extensions/models/litellm_model.py +449 -36
- agents/extensions/models/litellm_provider.py +3 -1
- agents/function_schema.py +47 -5
- agents/guardrail.py +16 -2
- agents/{handoffs.py → handoffs/__init__.py} +89 -47
- agents/handoffs/history.py +268 -0
- agents/items.py +237 -11
- agents/lifecycle.py +75 -14
- agents/mcp/server.py +280 -37
- agents/mcp/util.py +24 -3
- agents/memory/__init__.py +22 -2
- agents/memory/openai_conversations_session.py +91 -0
- agents/memory/openai_responses_compaction_session.py +249 -0
- agents/memory/session.py +19 -261
- agents/memory/sqlite_session.py +275 -0
- agents/memory/util.py +20 -0
- agents/model_settings.py +14 -3
- agents/models/__init__.py +13 -0
- agents/models/chatcmpl_converter.py +303 -50
- agents/models/chatcmpl_helpers.py +63 -0
- agents/models/chatcmpl_stream_handler.py +290 -68
- agents/models/default_models.py +58 -0
- agents/models/interface.py +4 -0
- agents/models/openai_chatcompletions.py +103 -49
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +162 -46
- agents/realtime/__init__.py +4 -0
- agents/realtime/_util.py +14 -3
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +53 -0
- agents/realtime/config.py +78 -10
- agents/realtime/events.py +18 -0
- agents/realtime/handoffs.py +2 -2
- agents/realtime/items.py +17 -1
- agents/realtime/model.py +13 -0
- agents/realtime/model_events.py +12 -0
- agents/realtime/model_inputs.py +18 -1
- agents/realtime/openai_realtime.py +696 -150
- agents/realtime/session.py +243 -23
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +949 -168
- agents/run_context.py +13 -2
- agents/stream_events.py +1 -0
- agents/strict_schema.py +14 -0
- agents/tool.py +413 -15
- agents/tool_context.py +22 -1
- agents/tool_guardrails.py +279 -0
- agents/tracing/__init__.py +2 -0
- agents/tracing/config.py +9 -0
- agents/tracing/create.py +4 -0
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/processors.py +65 -54
- agents/tracing/provider.py +64 -7
- agents/tracing/spans.py +105 -0
- agents/tracing/traces.py +116 -16
- agents/usage.py +134 -12
- agents/util/_json.py +19 -1
- agents/util/_transforms.py +12 -2
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +17 -9
- agents/voice/pipeline.py +2 -0
- agents/voice/pipeline_config.py +4 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
- openai_agents-0.6.8.dist-info/RECORD +134 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.8.dist-info/RECORD +0 -103
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
|
+
from contextvars import ContextVar
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, Union, cast, overload
|
|
7
8
|
|
|
8
|
-
from openai import
|
|
9
|
+
from openai import APIStatusError, AsyncOpenAI, AsyncStream, Omit, omit
|
|
9
10
|
from openai.types import ChatModel
|
|
10
11
|
from openai.types.responses import (
|
|
11
12
|
Response,
|
|
@@ -14,19 +15,20 @@ from openai.types.responses import (
|
|
|
14
15
|
ResponseStreamEvent,
|
|
15
16
|
ResponseTextConfigParam,
|
|
16
17
|
ToolParam,
|
|
17
|
-
WebSearchToolParam,
|
|
18
18
|
response_create_params,
|
|
19
19
|
)
|
|
20
20
|
from openai.types.responses.response_prompt_param import ResponsePromptParam
|
|
21
21
|
|
|
22
22
|
from .. import _debug
|
|
23
23
|
from ..agent_output import AgentOutputSchemaBase
|
|
24
|
+
from ..computer import AsyncComputer, Computer
|
|
24
25
|
from ..exceptions import UserError
|
|
25
26
|
from ..handoffs import Handoff
|
|
26
27
|
from ..items import ItemHelpers, ModelResponse, TResponseInputItem
|
|
27
28
|
from ..logger import logger
|
|
28
29
|
from ..model_settings import MCPToolChoice
|
|
29
30
|
from ..tool import (
|
|
31
|
+
ApplyPatchTool,
|
|
30
32
|
CodeInterpreterTool,
|
|
31
33
|
ComputerTool,
|
|
32
34
|
FileSearchTool,
|
|
@@ -34,12 +36,15 @@ from ..tool import (
|
|
|
34
36
|
HostedMCPTool,
|
|
35
37
|
ImageGenerationTool,
|
|
36
38
|
LocalShellTool,
|
|
39
|
+
ShellTool,
|
|
37
40
|
Tool,
|
|
38
41
|
WebSearchTool,
|
|
39
42
|
)
|
|
40
43
|
from ..tracing import SpanError, response_span
|
|
41
44
|
from ..usage import Usage
|
|
45
|
+
from ..util._json import _to_dump_compatible
|
|
42
46
|
from ..version import __version__
|
|
47
|
+
from .fake_id import FAKE_RESPONSES_ID
|
|
43
48
|
from .interface import Model, ModelTracing
|
|
44
49
|
|
|
45
50
|
if TYPE_CHECKING:
|
|
@@ -49,6 +54,11 @@ if TYPE_CHECKING:
|
|
|
49
54
|
_USER_AGENT = f"Agents/Python {__version__}"
|
|
50
55
|
_HEADERS = {"User-Agent": _USER_AGENT}
|
|
51
56
|
|
|
57
|
+
# Override headers used by the Responses API.
|
|
58
|
+
_HEADERS_OVERRIDE: ContextVar[dict[str, str] | None] = ContextVar(
|
|
59
|
+
"openai_responses_headers_override", default=None
|
|
60
|
+
)
|
|
61
|
+
|
|
52
62
|
|
|
53
63
|
class OpenAIResponsesModel(Model):
|
|
54
64
|
"""
|
|
@@ -59,12 +69,15 @@ class OpenAIResponsesModel(Model):
|
|
|
59
69
|
self,
|
|
60
70
|
model: str | ChatModel,
|
|
61
71
|
openai_client: AsyncOpenAI,
|
|
72
|
+
*,
|
|
73
|
+
model_is_explicit: bool = True,
|
|
62
74
|
) -> None:
|
|
63
75
|
self.model = model
|
|
76
|
+
self._model_is_explicit = model_is_explicit
|
|
64
77
|
self._client = openai_client
|
|
65
78
|
|
|
66
|
-
def
|
|
67
|
-
return value if value is not None else
|
|
79
|
+
def _non_null_or_omit(self, value: Any) -> Any:
|
|
80
|
+
return value if value is not None else omit
|
|
68
81
|
|
|
69
82
|
async def get_response(
|
|
70
83
|
self,
|
|
@@ -75,7 +88,8 @@ class OpenAIResponsesModel(Model):
|
|
|
75
88
|
output_schema: AgentOutputSchemaBase | None,
|
|
76
89
|
handoffs: list[Handoff],
|
|
77
90
|
tracing: ModelTracing,
|
|
78
|
-
previous_response_id: str | None,
|
|
91
|
+
previous_response_id: str | None = None,
|
|
92
|
+
conversation_id: str | None = None,
|
|
79
93
|
prompt: ResponsePromptParam | None = None,
|
|
80
94
|
) -> ModelResponse:
|
|
81
95
|
with response_span(disabled=tracing.is_disabled()) as span_response:
|
|
@@ -87,7 +101,8 @@ class OpenAIResponsesModel(Model):
|
|
|
87
101
|
tools,
|
|
88
102
|
output_schema,
|
|
89
103
|
handoffs,
|
|
90
|
-
previous_response_id,
|
|
104
|
+
previous_response_id=previous_response_id,
|
|
105
|
+
conversation_id=conversation_id,
|
|
91
106
|
stream=False,
|
|
92
107
|
prompt=prompt,
|
|
93
108
|
)
|
|
@@ -150,7 +165,8 @@ class OpenAIResponsesModel(Model):
|
|
|
150
165
|
output_schema: AgentOutputSchemaBase | None,
|
|
151
166
|
handoffs: list[Handoff],
|
|
152
167
|
tracing: ModelTracing,
|
|
153
|
-
previous_response_id: str | None,
|
|
168
|
+
previous_response_id: str | None = None,
|
|
169
|
+
conversation_id: str | None = None,
|
|
154
170
|
prompt: ResponsePromptParam | None = None,
|
|
155
171
|
) -> AsyncIterator[ResponseStreamEvent]:
|
|
156
172
|
"""
|
|
@@ -165,7 +181,8 @@ class OpenAIResponsesModel(Model):
|
|
|
165
181
|
tools,
|
|
166
182
|
output_schema,
|
|
167
183
|
handoffs,
|
|
168
|
-
previous_response_id,
|
|
184
|
+
previous_response_id=previous_response_id,
|
|
185
|
+
conversation_id=conversation_id,
|
|
169
186
|
stream=True,
|
|
170
187
|
prompt=prompt,
|
|
171
188
|
)
|
|
@@ -203,6 +220,7 @@ class OpenAIResponsesModel(Model):
|
|
|
203
220
|
output_schema: AgentOutputSchemaBase | None,
|
|
204
221
|
handoffs: list[Handoff],
|
|
205
222
|
previous_response_id: str | None,
|
|
223
|
+
conversation_id: str | None,
|
|
206
224
|
stream: Literal[True],
|
|
207
225
|
prompt: ResponsePromptParam | None = None,
|
|
208
226
|
) -> AsyncStream[ResponseStreamEvent]: ...
|
|
@@ -217,6 +235,7 @@ class OpenAIResponsesModel(Model):
|
|
|
217
235
|
output_schema: AgentOutputSchemaBase | None,
|
|
218
236
|
handoffs: list[Handoff],
|
|
219
237
|
previous_response_id: str | None,
|
|
238
|
+
conversation_id: str | None,
|
|
220
239
|
stream: Literal[False],
|
|
221
240
|
prompt: ResponsePromptParam | None = None,
|
|
222
241
|
) -> Response: ...
|
|
@@ -229,23 +248,32 @@ class OpenAIResponsesModel(Model):
|
|
|
229
248
|
tools: list[Tool],
|
|
230
249
|
output_schema: AgentOutputSchemaBase | None,
|
|
231
250
|
handoffs: list[Handoff],
|
|
232
|
-
previous_response_id: str | None,
|
|
251
|
+
previous_response_id: str | None = None,
|
|
252
|
+
conversation_id: str | None = None,
|
|
233
253
|
stream: Literal[True] | Literal[False] = False,
|
|
234
254
|
prompt: ResponsePromptParam | None = None,
|
|
235
255
|
) -> Response | AsyncStream[ResponseStreamEvent]:
|
|
236
256
|
list_input = ItemHelpers.input_to_new_input_list(input)
|
|
257
|
+
list_input = _to_dump_compatible(list_input)
|
|
258
|
+
list_input = self._remove_openai_responses_api_incompatible_fields(list_input)
|
|
237
259
|
|
|
238
|
-
parallel_tool_calls
|
|
239
|
-
True
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
260
|
+
if model_settings.parallel_tool_calls and tools:
|
|
261
|
+
parallel_tool_calls: bool | Omit = True
|
|
262
|
+
elif model_settings.parallel_tool_calls is False:
|
|
263
|
+
parallel_tool_calls = False
|
|
264
|
+
else:
|
|
265
|
+
parallel_tool_calls = omit
|
|
245
266
|
|
|
246
267
|
tool_choice = Converter.convert_tool_choice(model_settings.tool_choice)
|
|
247
268
|
converted_tools = Converter.convert_tools(tools, handoffs)
|
|
269
|
+
converted_tools_payload = _to_dump_compatible(converted_tools.tools)
|
|
248
270
|
response_format = Converter.get_response_format(output_schema)
|
|
271
|
+
should_omit_model = prompt is not None and not self._model_is_explicit
|
|
272
|
+
model_param: str | ChatModel | Omit = self.model if not should_omit_model else omit
|
|
273
|
+
should_omit_tools = prompt is not None and len(converted_tools_payload) == 0
|
|
274
|
+
tools_param: list[ToolParam] | Omit = (
|
|
275
|
+
converted_tools_payload if not should_omit_tools else omit
|
|
276
|
+
)
|
|
249
277
|
|
|
250
278
|
include_set: set[str] = set(converted_tools.includes)
|
|
251
279
|
if model_settings.response_include is not None:
|
|
@@ -257,55 +285,124 @@ class OpenAIResponsesModel(Model):
|
|
|
257
285
|
if _debug.DONT_LOG_MODEL_DATA:
|
|
258
286
|
logger.debug("Calling LLM")
|
|
259
287
|
else:
|
|
288
|
+
input_json = json.dumps(
|
|
289
|
+
list_input,
|
|
290
|
+
indent=2,
|
|
291
|
+
ensure_ascii=False,
|
|
292
|
+
)
|
|
293
|
+
tools_json = json.dumps(
|
|
294
|
+
converted_tools_payload,
|
|
295
|
+
indent=2,
|
|
296
|
+
ensure_ascii=False,
|
|
297
|
+
)
|
|
260
298
|
logger.debug(
|
|
261
299
|
f"Calling LLM {self.model} with input:\n"
|
|
262
|
-
f"{
|
|
263
|
-
f"Tools:\n{
|
|
300
|
+
f"{input_json}\n"
|
|
301
|
+
f"Tools:\n{tools_json}\n"
|
|
264
302
|
f"Stream: {stream}\n"
|
|
265
303
|
f"Tool choice: {tool_choice}\n"
|
|
266
304
|
f"Response format: {response_format}\n"
|
|
267
305
|
f"Previous response id: {previous_response_id}\n"
|
|
306
|
+
f"Conversation id: {conversation_id}\n"
|
|
268
307
|
)
|
|
269
308
|
|
|
270
309
|
extra_args = dict(model_settings.extra_args or {})
|
|
271
310
|
if model_settings.top_logprobs is not None:
|
|
272
311
|
extra_args["top_logprobs"] = model_settings.top_logprobs
|
|
273
312
|
if model_settings.verbosity is not None:
|
|
274
|
-
if response_format
|
|
313
|
+
if response_format is not omit:
|
|
275
314
|
response_format["verbosity"] = model_settings.verbosity # type: ignore [index]
|
|
276
315
|
else:
|
|
277
316
|
response_format = {"verbosity": model_settings.verbosity}
|
|
278
317
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
318
|
+
stream_param: Literal[True] | Omit = True if stream else omit
|
|
319
|
+
|
|
320
|
+
response = await self._client.responses.create(
|
|
321
|
+
previous_response_id=self._non_null_or_omit(previous_response_id),
|
|
322
|
+
conversation=self._non_null_or_omit(conversation_id),
|
|
323
|
+
instructions=self._non_null_or_omit(system_instructions),
|
|
324
|
+
model=model_param,
|
|
283
325
|
input=list_input,
|
|
284
326
|
include=include,
|
|
285
|
-
tools=
|
|
286
|
-
prompt=self.
|
|
287
|
-
temperature=self.
|
|
288
|
-
top_p=self.
|
|
289
|
-
truncation=self.
|
|
290
|
-
max_output_tokens=self.
|
|
327
|
+
tools=tools_param,
|
|
328
|
+
prompt=self._non_null_or_omit(prompt),
|
|
329
|
+
temperature=self._non_null_or_omit(model_settings.temperature),
|
|
330
|
+
top_p=self._non_null_or_omit(model_settings.top_p),
|
|
331
|
+
truncation=self._non_null_or_omit(model_settings.truncation),
|
|
332
|
+
max_output_tokens=self._non_null_or_omit(model_settings.max_tokens),
|
|
291
333
|
tool_choice=tool_choice,
|
|
292
334
|
parallel_tool_calls=parallel_tool_calls,
|
|
293
|
-
stream=
|
|
294
|
-
extra_headers=
|
|
335
|
+
stream=cast(Any, stream_param),
|
|
336
|
+
extra_headers=self._merge_headers(model_settings),
|
|
295
337
|
extra_query=model_settings.extra_query,
|
|
296
338
|
extra_body=model_settings.extra_body,
|
|
297
339
|
text=response_format,
|
|
298
|
-
store=self.
|
|
299
|
-
|
|
300
|
-
|
|
340
|
+
store=self._non_null_or_omit(model_settings.store),
|
|
341
|
+
prompt_cache_retention=self._non_null_or_omit(model_settings.prompt_cache_retention),
|
|
342
|
+
reasoning=self._non_null_or_omit(model_settings.reasoning),
|
|
343
|
+
metadata=self._non_null_or_omit(model_settings.metadata),
|
|
301
344
|
**extra_args,
|
|
302
345
|
)
|
|
346
|
+
return cast(Union[Response, AsyncStream[ResponseStreamEvent]], response)
|
|
347
|
+
|
|
348
|
+
def _remove_openai_responses_api_incompatible_fields(self, list_input: list[Any]) -> list[Any]:
|
|
349
|
+
"""
|
|
350
|
+
Remove or transform input items that are incompatible with the OpenAI Responses API.
|
|
351
|
+
|
|
352
|
+
This data transformation does not always guarantee that items from other provider
|
|
353
|
+
interactions are accepted by the OpenAI Responses API.
|
|
354
|
+
|
|
355
|
+
Only items with truthy provider_data are processed.
|
|
356
|
+
This function handles the following incompatibilities:
|
|
357
|
+
- provider_data: Removes fields specific to other providers (e.g., Gemini, Claude).
|
|
358
|
+
- Fake IDs: Removes temporary IDs (FAKE_RESPONSES_ID) that should not be sent to OpenAI.
|
|
359
|
+
- Reasoning items: Filters out provider-specific reasoning items entirely.
|
|
360
|
+
"""
|
|
361
|
+
# Early return optimization: if no item has provider_data, return unchanged.
|
|
362
|
+
has_provider_data = any(
|
|
363
|
+
isinstance(item, dict) and item.get("provider_data") for item in list_input
|
|
364
|
+
)
|
|
365
|
+
if not has_provider_data:
|
|
366
|
+
return list_input
|
|
367
|
+
|
|
368
|
+
result = []
|
|
369
|
+
for item in list_input:
|
|
370
|
+
cleaned = self._clean_item_for_openai(item)
|
|
371
|
+
if cleaned is not None:
|
|
372
|
+
result.append(cleaned)
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
def _clean_item_for_openai(self, item: Any) -> Any | None:
|
|
376
|
+
# Only process dict items
|
|
377
|
+
if not isinstance(item, dict):
|
|
378
|
+
return item
|
|
379
|
+
|
|
380
|
+
# Filter out reasoning items with provider_data (provider-specific reasoning).
|
|
381
|
+
if item.get("type") == "reasoning" and item.get("provider_data"):
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# Remove fake response ID.
|
|
385
|
+
if item.get("id") == FAKE_RESPONSES_ID:
|
|
386
|
+
del item["id"]
|
|
387
|
+
|
|
388
|
+
# Remove provider_data field.
|
|
389
|
+
if "provider_data" in item:
|
|
390
|
+
del item["provider_data"]
|
|
391
|
+
|
|
392
|
+
return item
|
|
303
393
|
|
|
304
394
|
def _get_client(self) -> AsyncOpenAI:
|
|
305
395
|
if self._client is None:
|
|
306
396
|
self._client = AsyncOpenAI()
|
|
307
397
|
return self._client
|
|
308
398
|
|
|
399
|
+
def _merge_headers(self, model_settings: ModelSettings):
|
|
400
|
+
return {
|
|
401
|
+
**_HEADERS,
|
|
402
|
+
**(model_settings.extra_headers or {}),
|
|
403
|
+
**(_HEADERS_OVERRIDE.get() or {}),
|
|
404
|
+
}
|
|
405
|
+
|
|
309
406
|
|
|
310
407
|
@dataclass
|
|
311
408
|
class ConvertedTools:
|
|
@@ -317,9 +414,9 @@ class Converter:
|
|
|
317
414
|
@classmethod
|
|
318
415
|
def convert_tool_choice(
|
|
319
416
|
cls, tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None
|
|
320
|
-
) -> response_create_params.ToolChoice |
|
|
417
|
+
) -> response_create_params.ToolChoice | Omit:
|
|
321
418
|
if tool_choice is None:
|
|
322
|
-
return
|
|
419
|
+
return omit
|
|
323
420
|
elif isinstance(tool_choice, MCPToolChoice):
|
|
324
421
|
return {
|
|
325
422
|
"server_label": tool_choice.server_label,
|
|
@@ -336,6 +433,11 @@ class Converter:
|
|
|
336
433
|
return {
|
|
337
434
|
"type": "file_search",
|
|
338
435
|
}
|
|
436
|
+
elif tool_choice == "web_search":
|
|
437
|
+
return {
|
|
438
|
+
# TODO: revist the type: ignore comment when ToolChoice is updated in the future
|
|
439
|
+
"type": "web_search", # type: ignore[misc, return-value]
|
|
440
|
+
}
|
|
339
441
|
elif tool_choice == "web_search_preview":
|
|
340
442
|
return {
|
|
341
443
|
"type": "web_search_preview",
|
|
@@ -355,7 +457,7 @@ class Converter:
|
|
|
355
457
|
elif tool_choice == "mcp":
|
|
356
458
|
# Note that this is still here for backwards compatibility,
|
|
357
459
|
# but migrating to MCPToolChoice is recommended.
|
|
358
|
-
return {"type": "mcp"} # type: ignore
|
|
460
|
+
return {"type": "mcp"} # type: ignore[misc, return-value]
|
|
359
461
|
else:
|
|
360
462
|
return {
|
|
361
463
|
"type": "function",
|
|
@@ -365,9 +467,9 @@ class Converter:
|
|
|
365
467
|
@classmethod
|
|
366
468
|
def get_response_format(
|
|
367
469
|
cls, output_schema: AgentOutputSchemaBase | None
|
|
368
|
-
) -> ResponseTextConfigParam |
|
|
470
|
+
) -> ResponseTextConfigParam | Omit:
|
|
369
471
|
if output_schema is None or output_schema.is_plain_text():
|
|
370
|
-
return
|
|
472
|
+
return omit
|
|
371
473
|
else:
|
|
372
474
|
return {
|
|
373
475
|
"format": {
|
|
@@ -416,12 +518,13 @@ class Converter:
|
|
|
416
518
|
}
|
|
417
519
|
includes: ResponseIncludable | None = None
|
|
418
520
|
elif isinstance(tool, WebSearchTool):
|
|
419
|
-
|
|
420
|
-
|
|
521
|
+
# TODO: revist the type: ignore comment when ToolParam is updated in the future
|
|
522
|
+
converted_tool = {
|
|
523
|
+
"type": "web_search",
|
|
524
|
+
"filters": tool.filters.model_dump() if tool.filters is not None else None, # type: ignore [typeddict-item]
|
|
421
525
|
"user_location": tool.user_location,
|
|
422
526
|
"search_context_size": tool.search_context_size,
|
|
423
527
|
}
|
|
424
|
-
converted_tool = ws
|
|
425
528
|
includes = None
|
|
426
529
|
elif isinstance(tool, FileSearchTool):
|
|
427
530
|
converted_tool = {
|
|
@@ -437,16 +540,29 @@ class Converter:
|
|
|
437
540
|
|
|
438
541
|
includes = "file_search_call.results" if tool.include_search_results else None
|
|
439
542
|
elif isinstance(tool, ComputerTool):
|
|
543
|
+
computer = tool.computer
|
|
544
|
+
if not isinstance(computer, (Computer, AsyncComputer)):
|
|
545
|
+
raise UserError(
|
|
546
|
+
"Computer tool is not initialized for serialization. Call "
|
|
547
|
+
"resolve_computer({ tool, run_context }) with a run context first "
|
|
548
|
+
"when building payloads manually."
|
|
549
|
+
)
|
|
440
550
|
converted_tool = {
|
|
441
551
|
"type": "computer_use_preview",
|
|
442
|
-
"environment":
|
|
443
|
-
"display_width":
|
|
444
|
-
"display_height":
|
|
552
|
+
"environment": computer.environment,
|
|
553
|
+
"display_width": computer.dimensions[0],
|
|
554
|
+
"display_height": computer.dimensions[1],
|
|
445
555
|
}
|
|
446
556
|
includes = None
|
|
447
557
|
elif isinstance(tool, HostedMCPTool):
|
|
448
558
|
converted_tool = tool.tool_config
|
|
449
559
|
includes = None
|
|
560
|
+
elif isinstance(tool, ApplyPatchTool):
|
|
561
|
+
converted_tool = cast(ToolParam, {"type": "apply_patch"})
|
|
562
|
+
includes = None
|
|
563
|
+
elif isinstance(tool, ShellTool):
|
|
564
|
+
converted_tool = cast(ToolParam, {"type": "shell"})
|
|
565
|
+
includes = None
|
|
450
566
|
elif isinstance(tool, ImageGenerationTool):
|
|
451
567
|
converted_tool = tool.tool_config
|
|
452
568
|
includes = None
|
agents/realtime/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from .config import (
|
|
|
3
3
|
RealtimeAudioFormat,
|
|
4
4
|
RealtimeClientMessage,
|
|
5
5
|
RealtimeGuardrailsSettings,
|
|
6
|
+
RealtimeInputAudioNoiseReductionConfig,
|
|
6
7
|
RealtimeInputAudioTranscriptionConfig,
|
|
7
8
|
RealtimeModelName,
|
|
8
9
|
RealtimeModelTracingConfig,
|
|
@@ -83,6 +84,7 @@ from .model_inputs import (
|
|
|
83
84
|
)
|
|
84
85
|
from .openai_realtime import (
|
|
85
86
|
DEFAULT_MODEL_SETTINGS,
|
|
87
|
+
OpenAIRealtimeSIPModel,
|
|
86
88
|
OpenAIRealtimeWebSocketModel,
|
|
87
89
|
get_api_key,
|
|
88
90
|
)
|
|
@@ -101,6 +103,7 @@ __all__ = [
|
|
|
101
103
|
"RealtimeAudioFormat",
|
|
102
104
|
"RealtimeClientMessage",
|
|
103
105
|
"RealtimeGuardrailsSettings",
|
|
106
|
+
"RealtimeInputAudioNoiseReductionConfig",
|
|
104
107
|
"RealtimeInputAudioTranscriptionConfig",
|
|
105
108
|
"RealtimeModelName",
|
|
106
109
|
"RealtimeModelTracingConfig",
|
|
@@ -174,6 +177,7 @@ __all__ = [
|
|
|
174
177
|
"RealtimeModelUserInputMessage",
|
|
175
178
|
# OpenAI Realtime
|
|
176
179
|
"DEFAULT_MODEL_SETTINGS",
|
|
180
|
+
"OpenAIRealtimeSIPModel",
|
|
177
181
|
"OpenAIRealtimeWebSocketModel",
|
|
178
182
|
"get_api_key",
|
|
179
183
|
# Session
|
agents/realtime/_util.py
CHANGED
|
@@ -2,8 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from .config import RealtimeAudioFormat
|
|
4
4
|
|
|
5
|
+
PCM16_SAMPLE_RATE_HZ = 24_000
|
|
6
|
+
PCM16_SAMPLE_WIDTH_BYTES = 2
|
|
7
|
+
G711_SAMPLE_RATE_HZ = 8_000
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
def calculate_audio_length_ms(format: RealtimeAudioFormat | None, audio_bytes: bytes) -> float:
|
|
7
|
-
if
|
|
8
|
-
return
|
|
9
|
-
|
|
11
|
+
if not audio_bytes:
|
|
12
|
+
return 0.0
|
|
13
|
+
|
|
14
|
+
normalized_format = format.lower() if isinstance(format, str) else None
|
|
15
|
+
|
|
16
|
+
if normalized_format and normalized_format.startswith("g711"):
|
|
17
|
+
return (len(audio_bytes) / G711_SAMPLE_RATE_HZ) * 1000
|
|
18
|
+
|
|
19
|
+
samples = len(audio_bytes) / PCM16_SAMPLE_WIDTH_BYTES
|
|
20
|
+
return (samples / PCM16_SAMPLE_RATE_HZ) * 1000
|
agents/realtime/agent.py
CHANGED
|
@@ -6,6 +6,8 @@ from collections.abc import Awaitable
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any, Callable, Generic, cast
|
|
8
8
|
|
|
9
|
+
from agents.prompts import Prompt
|
|
10
|
+
|
|
9
11
|
from ..agent import AgentBase
|
|
10
12
|
from ..guardrail import OutputGuardrail
|
|
11
13
|
from ..handoffs import Handoff
|
|
@@ -55,6 +57,11 @@ class RealtimeAgent(AgentBase, Generic[TContext]):
|
|
|
55
57
|
return a string.
|
|
56
58
|
"""
|
|
57
59
|
|
|
60
|
+
prompt: Prompt | None = None
|
|
61
|
+
"""A prompt object. Prompts allow you to dynamically configure the instructions, tools
|
|
62
|
+
and other config for an agent outside of your code. Only usable with OpenAI models.
|
|
63
|
+
"""
|
|
64
|
+
|
|
58
65
|
handoffs: list[RealtimeAgent[Any] | Handoff[TContext, RealtimeAgent[Any]]] = field(
|
|
59
66
|
default_factory=list
|
|
60
67
|
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from openai.types.realtime.realtime_audio_formats import (
|
|
7
|
+
AudioPCM,
|
|
8
|
+
AudioPCMA,
|
|
9
|
+
AudioPCMU,
|
|
10
|
+
RealtimeAudioFormats,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ..logger import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_realtime_audio_format(
|
|
17
|
+
input_audio_format: str | RealtimeAudioFormats | Mapping[str, Any] | None,
|
|
18
|
+
) -> RealtimeAudioFormats | None:
|
|
19
|
+
format: RealtimeAudioFormats | None = None
|
|
20
|
+
if input_audio_format is not None:
|
|
21
|
+
if isinstance(input_audio_format, str):
|
|
22
|
+
if input_audio_format in ["pcm16", "audio/pcm", "pcm"]:
|
|
23
|
+
format = AudioPCM(type="audio/pcm", rate=24000)
|
|
24
|
+
elif input_audio_format in ["g711_ulaw", "audio/pcmu", "pcmu"]:
|
|
25
|
+
format = AudioPCMU(type="audio/pcmu")
|
|
26
|
+
elif input_audio_format in ["g711_alaw", "audio/pcma", "pcma"]:
|
|
27
|
+
format = AudioPCMA(type="audio/pcma")
|
|
28
|
+
else:
|
|
29
|
+
logger.debug(f"Unknown input_audio_format: {input_audio_format}")
|
|
30
|
+
elif isinstance(input_audio_format, Mapping):
|
|
31
|
+
fmt_type = input_audio_format.get("type")
|
|
32
|
+
rate = input_audio_format.get("rate")
|
|
33
|
+
if fmt_type == "audio/pcm":
|
|
34
|
+
pcm_rate: Literal[24000] | None
|
|
35
|
+
if isinstance(rate, (int, float)) and int(rate) == 24000:
|
|
36
|
+
pcm_rate = 24000
|
|
37
|
+
elif rate is None:
|
|
38
|
+
pcm_rate = 24000
|
|
39
|
+
else:
|
|
40
|
+
logger.debug(
|
|
41
|
+
f"Unknown pcm rate in input_audio_format mapping: {input_audio_format}"
|
|
42
|
+
)
|
|
43
|
+
pcm_rate = 24000
|
|
44
|
+
format = AudioPCM(type="audio/pcm", rate=pcm_rate)
|
|
45
|
+
elif fmt_type == "audio/pcmu":
|
|
46
|
+
format = AudioPCMU(type="audio/pcmu")
|
|
47
|
+
elif fmt_type == "audio/pcma":
|
|
48
|
+
format = AudioPCMA(type="audio/pcma")
|
|
49
|
+
else:
|
|
50
|
+
logger.debug(f"Unknown input_audio_format mapping: {input_audio_format}")
|
|
51
|
+
else:
|
|
52
|
+
format = input_audio_format
|
|
53
|
+
return format
|