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.
Files changed (96) hide show
  1. agents/__init__.py +105 -4
  2. agents/_debug.py +15 -4
  3. agents/_run_impl.py +1203 -96
  4. agents/agent.py +164 -19
  5. agents/apply_diff.py +329 -0
  6. agents/editor.py +47 -0
  7. agents/exceptions.py +35 -0
  8. agents/extensions/experimental/__init__.py +6 -0
  9. agents/extensions/experimental/codex/__init__.py +92 -0
  10. agents/extensions/experimental/codex/codex.py +89 -0
  11. agents/extensions/experimental/codex/codex_options.py +35 -0
  12. agents/extensions/experimental/codex/codex_tool.py +1142 -0
  13. agents/extensions/experimental/codex/events.py +162 -0
  14. agents/extensions/experimental/codex/exec.py +263 -0
  15. agents/extensions/experimental/codex/items.py +245 -0
  16. agents/extensions/experimental/codex/output_schema_file.py +50 -0
  17. agents/extensions/experimental/codex/payloads.py +31 -0
  18. agents/extensions/experimental/codex/thread.py +214 -0
  19. agents/extensions/experimental/codex/thread_options.py +54 -0
  20. agents/extensions/experimental/codex/turn_options.py +36 -0
  21. agents/extensions/handoff_filters.py +13 -1
  22. agents/extensions/memory/__init__.py +120 -0
  23. agents/extensions/memory/advanced_sqlite_session.py +1285 -0
  24. agents/extensions/memory/async_sqlite_session.py +239 -0
  25. agents/extensions/memory/dapr_session.py +423 -0
  26. agents/extensions/memory/encrypt_session.py +185 -0
  27. agents/extensions/memory/redis_session.py +261 -0
  28. agents/extensions/memory/sqlalchemy_session.py +334 -0
  29. agents/extensions/models/litellm_model.py +449 -36
  30. agents/extensions/models/litellm_provider.py +3 -1
  31. agents/function_schema.py +47 -5
  32. agents/guardrail.py +16 -2
  33. agents/{handoffs.py → handoffs/__init__.py} +89 -47
  34. agents/handoffs/history.py +268 -0
  35. agents/items.py +237 -11
  36. agents/lifecycle.py +75 -14
  37. agents/mcp/server.py +280 -37
  38. agents/mcp/util.py +24 -3
  39. agents/memory/__init__.py +22 -2
  40. agents/memory/openai_conversations_session.py +91 -0
  41. agents/memory/openai_responses_compaction_session.py +249 -0
  42. agents/memory/session.py +19 -261
  43. agents/memory/sqlite_session.py +275 -0
  44. agents/memory/util.py +20 -0
  45. agents/model_settings.py +14 -3
  46. agents/models/__init__.py +13 -0
  47. agents/models/chatcmpl_converter.py +303 -50
  48. agents/models/chatcmpl_helpers.py +63 -0
  49. agents/models/chatcmpl_stream_handler.py +290 -68
  50. agents/models/default_models.py +58 -0
  51. agents/models/interface.py +4 -0
  52. agents/models/openai_chatcompletions.py +103 -49
  53. agents/models/openai_provider.py +10 -4
  54. agents/models/openai_responses.py +162 -46
  55. agents/realtime/__init__.py +4 -0
  56. agents/realtime/_util.py +14 -3
  57. agents/realtime/agent.py +7 -0
  58. agents/realtime/audio_formats.py +53 -0
  59. agents/realtime/config.py +78 -10
  60. agents/realtime/events.py +18 -0
  61. agents/realtime/handoffs.py +2 -2
  62. agents/realtime/items.py +17 -1
  63. agents/realtime/model.py +13 -0
  64. agents/realtime/model_events.py +12 -0
  65. agents/realtime/model_inputs.py +18 -1
  66. agents/realtime/openai_realtime.py +696 -150
  67. agents/realtime/session.py +243 -23
  68. agents/repl.py +7 -3
  69. agents/result.py +197 -38
  70. agents/run.py +949 -168
  71. agents/run_context.py +13 -2
  72. agents/stream_events.py +1 -0
  73. agents/strict_schema.py +14 -0
  74. agents/tool.py +413 -15
  75. agents/tool_context.py +22 -1
  76. agents/tool_guardrails.py +279 -0
  77. agents/tracing/__init__.py +2 -0
  78. agents/tracing/config.py +9 -0
  79. agents/tracing/create.py +4 -0
  80. agents/tracing/processor_interface.py +84 -11
  81. agents/tracing/processors.py +65 -54
  82. agents/tracing/provider.py +64 -7
  83. agents/tracing/spans.py +105 -0
  84. agents/tracing/traces.py +116 -16
  85. agents/usage.py +134 -12
  86. agents/util/_json.py +19 -1
  87. agents/util/_transforms.py +12 -2
  88. agents/voice/input.py +5 -4
  89. agents/voice/models/openai_stt.py +17 -9
  90. agents/voice/pipeline.py +2 -0
  91. agents/voice/pipeline_config.py +4 -0
  92. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
  93. openai_agents-0.6.8.dist-info/RECORD +134 -0
  94. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
  95. openai_agents-0.2.8.dist-info/RECORD +0 -103
  96. {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 NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
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 _non_null_or_not_given(self, value: Any) -> Any:
67
- return value if value is not None else NOT_GIVEN
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
- if model_settings.parallel_tool_calls and tools and len(tools) > 0
241
- else False
242
- if model_settings.parallel_tool_calls is False
243
- else NOT_GIVEN
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"{json.dumps(list_input, indent=2, ensure_ascii=False)}\n"
263
- f"Tools:\n{json.dumps(converted_tools.tools, indent=2, ensure_ascii=False)}\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 != NOT_GIVEN:
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
- return await self._client.responses.create(
280
- previous_response_id=self._non_null_or_not_given(previous_response_id),
281
- instructions=self._non_null_or_not_given(system_instructions),
282
- model=self.model,
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=converted_tools.tools,
286
- prompt=self._non_null_or_not_given(prompt),
287
- temperature=self._non_null_or_not_given(model_settings.temperature),
288
- top_p=self._non_null_or_not_given(model_settings.top_p),
289
- truncation=self._non_null_or_not_given(model_settings.truncation),
290
- max_output_tokens=self._non_null_or_not_given(model_settings.max_tokens),
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=stream,
294
- extra_headers={**_HEADERS, **(model_settings.extra_headers or {})},
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._non_null_or_not_given(model_settings.store),
299
- reasoning=self._non_null_or_not_given(model_settings.reasoning),
300
- metadata=self._non_null_or_not_given(model_settings.metadata),
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 | NotGiven:
417
+ ) -> response_create_params.ToolChoice | Omit:
321
418
  if tool_choice is None:
322
- return NOT_GIVEN
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 [typeddict-item]
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 | NotGiven:
470
+ ) -> ResponseTextConfigParam | Omit:
369
471
  if output_schema is None or output_schema.is_plain_text():
370
- return NOT_GIVEN
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
- ws: WebSearchToolParam = {
420
- "type": "web_search_preview",
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": tool.computer.environment,
443
- "display_width": tool.computer.dimensions[0],
444
- "display_height": tool.computer.dimensions[1],
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
@@ -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 format and format.startswith("g711"):
8
- return (len(audio_bytes) / 8000) * 1000
9
- return (len(audio_bytes) / 24 / 2) * 1000
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