openai-agents 0.2.11__py3-none-any.whl → 0.3.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/_debug.py +15 -4
- agents/_run_impl.py +34 -37
- agents/extensions/models/litellm_model.py +20 -5
- agents/memory/__init__.py +2 -0
- agents/memory/openai_conversations_session.py +0 -3
- agents/memory/util.py +20 -0
- agents/models/openai_chatcompletions.py +17 -2
- agents/models/openai_responses.py +16 -3
- agents/realtime/_util.py +1 -1
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +29 -0
- agents/realtime/config.py +22 -4
- agents/realtime/items.py +17 -1
- agents/realtime/model_inputs.py +15 -1
- agents/realtime/openai_realtime.py +411 -130
- agents/realtime/session.py +167 -14
- agents/run.py +54 -35
- agents/tool.py +1 -1
- agents/util/_json.py +19 -1
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +6 -4
- {openai_agents-0.2.11.dist-info → openai_agents-0.3.0.dist-info}/METADATA +2 -2
- {openai_agents-0.2.11.dist-info → openai_agents-0.3.0.dist-info}/RECORD +25 -23
- {openai_agents-0.2.11.dist-info → openai_agents-0.3.0.dist-info}/WHEEL +0 -0
- {openai_agents-0.2.11.dist-info → openai_agents-0.3.0.dist-info}/licenses/LICENSE +0 -0
agents/realtime/session.py
CHANGED
|
@@ -35,7 +35,16 @@ from .events import (
|
|
|
35
35
|
RealtimeToolStart,
|
|
36
36
|
)
|
|
37
37
|
from .handoffs import realtime_handoff
|
|
38
|
-
from .items import
|
|
38
|
+
from .items import (
|
|
39
|
+
AssistantAudio,
|
|
40
|
+
AssistantMessageItem,
|
|
41
|
+
AssistantText,
|
|
42
|
+
InputAudio,
|
|
43
|
+
InputImage,
|
|
44
|
+
InputText,
|
|
45
|
+
RealtimeItem,
|
|
46
|
+
UserMessageItem,
|
|
47
|
+
)
|
|
39
48
|
from .model import RealtimeModel, RealtimeModelConfig, RealtimeModelListener
|
|
40
49
|
from .model_events import (
|
|
41
50
|
RealtimeModelEvent,
|
|
@@ -95,6 +104,12 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
95
104
|
self._history: list[RealtimeItem] = []
|
|
96
105
|
self._model_config = model_config or {}
|
|
97
106
|
self._run_config = run_config or {}
|
|
107
|
+
initial_model_settings = self._model_config.get("initial_model_settings")
|
|
108
|
+
run_config_settings = self._run_config.get("model_settings")
|
|
109
|
+
self._base_model_settings: RealtimeSessionModelSettings = {
|
|
110
|
+
**(run_config_settings or {}),
|
|
111
|
+
**(initial_model_settings or {}),
|
|
112
|
+
}
|
|
98
113
|
self._event_queue: asyncio.Queue[RealtimeSessionEvent] = asyncio.Queue()
|
|
99
114
|
self._closed = False
|
|
100
115
|
self._stored_exception: Exception | None = None
|
|
@@ -224,10 +239,17 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
224
239
|
)
|
|
225
240
|
)
|
|
226
241
|
elif event.type == "input_audio_transcription_completed":
|
|
242
|
+
prev_len = len(self._history)
|
|
227
243
|
self._history = RealtimeSession._get_new_history(self._history, event)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
)
|
|
244
|
+
# If a new user item was appended (no existing item),
|
|
245
|
+
# emit history_added for incremental UIs.
|
|
246
|
+
if len(self._history) > prev_len and len(self._history) > 0:
|
|
247
|
+
new_item = self._history[-1]
|
|
248
|
+
await self._put_event(RealtimeHistoryAdded(info=self._event_info, item=new_item))
|
|
249
|
+
else:
|
|
250
|
+
await self._put_event(
|
|
251
|
+
RealtimeHistoryUpdated(info=self._event_info, history=self._history)
|
|
252
|
+
)
|
|
231
253
|
elif event.type == "input_audio_timeout_triggered":
|
|
232
254
|
await self._put_event(
|
|
233
255
|
RealtimeInputAudioTimeoutTriggered(
|
|
@@ -242,6 +264,13 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
242
264
|
self._item_guardrail_run_counts[item_id] = 0
|
|
243
265
|
|
|
244
266
|
self._item_transcripts[item_id] += event.delta
|
|
267
|
+
self._history = self._get_new_history(
|
|
268
|
+
self._history,
|
|
269
|
+
AssistantMessageItem(
|
|
270
|
+
item_id=item_id,
|
|
271
|
+
content=[AssistantAudio(transcript=self._item_transcripts[item_id])],
|
|
272
|
+
),
|
|
273
|
+
)
|
|
245
274
|
|
|
246
275
|
# Check if we should run guardrails based on debounce threshold
|
|
247
276
|
current_length = len(self._item_transcripts[item_id])
|
|
@@ -291,7 +320,7 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
291
320
|
|
|
292
321
|
# If still missing and this is an assistant item, fall back to
|
|
293
322
|
# accumulated transcript deltas tracked during the turn.
|
|
294
|
-
if
|
|
323
|
+
if incoming_item.role == "assistant":
|
|
295
324
|
preserved = self._item_transcripts.get(incoming_item.item_id)
|
|
296
325
|
|
|
297
326
|
if preserved:
|
|
@@ -456,9 +485,9 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
456
485
|
old_history: list[RealtimeItem],
|
|
457
486
|
event: RealtimeModelInputAudioTranscriptionCompletedEvent | RealtimeItem,
|
|
458
487
|
) -> list[RealtimeItem]:
|
|
459
|
-
# Merge transcript into placeholder input_audio message.
|
|
460
488
|
if isinstance(event, RealtimeModelInputAudioTranscriptionCompletedEvent):
|
|
461
489
|
new_history: list[RealtimeItem] = []
|
|
490
|
+
existing_item_found = False
|
|
462
491
|
for item in old_history:
|
|
463
492
|
if item.item_id == event.item_id and item.type == "message" and item.role == "user":
|
|
464
493
|
content: list[InputText | InputAudio] = []
|
|
@@ -471,11 +500,18 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
471
500
|
new_history.append(
|
|
472
501
|
item.model_copy(update={"content": content, "status": "completed"})
|
|
473
502
|
)
|
|
503
|
+
existing_item_found = True
|
|
474
504
|
else:
|
|
475
505
|
new_history.append(item)
|
|
506
|
+
|
|
507
|
+
if existing_item_found is False:
|
|
508
|
+
new_history.append(
|
|
509
|
+
UserMessageItem(
|
|
510
|
+
item_id=event.item_id, content=[InputText(text=event.transcript)]
|
|
511
|
+
)
|
|
512
|
+
)
|
|
476
513
|
return new_history
|
|
477
514
|
|
|
478
|
-
# Otherwise it's just a new item
|
|
479
515
|
# TODO (rm) Add support for audio storage config
|
|
480
516
|
|
|
481
517
|
# If the item already exists, update it
|
|
@@ -484,8 +520,122 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
484
520
|
)
|
|
485
521
|
if existing_index is not None:
|
|
486
522
|
new_history = old_history.copy()
|
|
487
|
-
|
|
523
|
+
if event.type == "message" and event.content is not None and len(event.content) > 0:
|
|
524
|
+
existing_item = old_history[existing_index]
|
|
525
|
+
if existing_item.type == "message":
|
|
526
|
+
# Merge content preserving existing transcript/text when incoming entry is empty
|
|
527
|
+
if event.role == "assistant" and existing_item.role == "assistant":
|
|
528
|
+
assistant_existing_content = existing_item.content
|
|
529
|
+
assistant_incoming = event.content
|
|
530
|
+
assistant_new_content: list[AssistantText | AssistantAudio] = []
|
|
531
|
+
for idx, ac in enumerate(assistant_incoming):
|
|
532
|
+
if idx >= len(assistant_existing_content):
|
|
533
|
+
assistant_new_content.append(ac)
|
|
534
|
+
continue
|
|
535
|
+
assistant_current = assistant_existing_content[idx]
|
|
536
|
+
if ac.type == "audio":
|
|
537
|
+
if ac.transcript is None:
|
|
538
|
+
assistant_new_content.append(assistant_current)
|
|
539
|
+
else:
|
|
540
|
+
assistant_new_content.append(ac)
|
|
541
|
+
else: # text
|
|
542
|
+
cur_text = (
|
|
543
|
+
assistant_current.text
|
|
544
|
+
if isinstance(assistant_current, AssistantText)
|
|
545
|
+
else None
|
|
546
|
+
)
|
|
547
|
+
if cur_text is not None and ac.text is None:
|
|
548
|
+
assistant_new_content.append(assistant_current)
|
|
549
|
+
else:
|
|
550
|
+
assistant_new_content.append(ac)
|
|
551
|
+
updated_assistant = event.model_copy(
|
|
552
|
+
update={"content": assistant_new_content}
|
|
553
|
+
)
|
|
554
|
+
new_history[existing_index] = updated_assistant
|
|
555
|
+
elif event.role == "user" and existing_item.role == "user":
|
|
556
|
+
user_existing_content = existing_item.content
|
|
557
|
+
user_incoming = event.content
|
|
558
|
+
|
|
559
|
+
# Start from incoming content (prefer latest fields)
|
|
560
|
+
user_new_content: list[InputText | InputAudio | InputImage] = list(
|
|
561
|
+
user_incoming
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Merge by type with special handling for images and transcripts
|
|
565
|
+
def _image_url_str(val: object) -> str | None:
|
|
566
|
+
if isinstance(val, InputImage):
|
|
567
|
+
return val.image_url or None
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
# 1) Preserve any existing images that are missing from the incoming payload
|
|
571
|
+
incoming_image_urls: set[str] = set()
|
|
572
|
+
for part in user_incoming:
|
|
573
|
+
if isinstance(part, InputImage):
|
|
574
|
+
u = _image_url_str(part)
|
|
575
|
+
if u:
|
|
576
|
+
incoming_image_urls.add(u)
|
|
577
|
+
|
|
578
|
+
missing_images: list[InputImage] = []
|
|
579
|
+
for part in user_existing_content:
|
|
580
|
+
if isinstance(part, InputImage):
|
|
581
|
+
u = _image_url_str(part)
|
|
582
|
+
if u and u not in incoming_image_urls:
|
|
583
|
+
missing_images.append(part)
|
|
584
|
+
|
|
585
|
+
# Insert missing images at the beginning to keep them visible and stable
|
|
586
|
+
if missing_images:
|
|
587
|
+
user_new_content = missing_images + user_new_content
|
|
588
|
+
|
|
589
|
+
# 2) For text/audio entries, preserve existing when incoming entry is empty
|
|
590
|
+
merged: list[InputText | InputAudio | InputImage] = []
|
|
591
|
+
for idx, uc in enumerate(user_new_content):
|
|
592
|
+
if uc.type == "input_audio":
|
|
593
|
+
# Attempt to preserve transcript if empty
|
|
594
|
+
transcript = getattr(uc, "transcript", None)
|
|
595
|
+
if transcript is None and idx < len(user_existing_content):
|
|
596
|
+
prev = user_existing_content[idx]
|
|
597
|
+
if isinstance(prev, InputAudio) and prev.transcript is not None:
|
|
598
|
+
uc = uc.model_copy(update={"transcript": prev.transcript})
|
|
599
|
+
merged.append(uc)
|
|
600
|
+
elif uc.type == "input_text":
|
|
601
|
+
text = getattr(uc, "text", None)
|
|
602
|
+
if (text is None or text == "") and idx < len(
|
|
603
|
+
user_existing_content
|
|
604
|
+
):
|
|
605
|
+
prev = user_existing_content[idx]
|
|
606
|
+
if isinstance(prev, InputText) and prev.text:
|
|
607
|
+
uc = uc.model_copy(update={"text": prev.text})
|
|
608
|
+
merged.append(uc)
|
|
609
|
+
else:
|
|
610
|
+
merged.append(uc)
|
|
611
|
+
|
|
612
|
+
updated_user = event.model_copy(update={"content": merged})
|
|
613
|
+
new_history[existing_index] = updated_user
|
|
614
|
+
elif event.role == "system" and existing_item.role == "system":
|
|
615
|
+
system_existing_content = existing_item.content
|
|
616
|
+
system_incoming = event.content
|
|
617
|
+
# Prefer existing non-empty text when incoming is empty
|
|
618
|
+
system_new_content: list[InputText] = []
|
|
619
|
+
for idx, sc in enumerate(system_incoming):
|
|
620
|
+
if idx >= len(system_existing_content):
|
|
621
|
+
system_new_content.append(sc)
|
|
622
|
+
continue
|
|
623
|
+
system_current = system_existing_content[idx]
|
|
624
|
+
cur_text = system_current.text
|
|
625
|
+
if cur_text is not None and sc.text is None:
|
|
626
|
+
system_new_content.append(system_current)
|
|
627
|
+
else:
|
|
628
|
+
system_new_content.append(sc)
|
|
629
|
+
updated_system = event.model_copy(update={"content": system_new_content})
|
|
630
|
+
new_history[existing_index] = updated_system
|
|
631
|
+
else:
|
|
632
|
+
# Role changed or mismatched; just replace
|
|
633
|
+
new_history[existing_index] = event
|
|
634
|
+
else:
|
|
635
|
+
# If the existing item is not a message, just replace it.
|
|
636
|
+
new_history[existing_index] = event
|
|
488
637
|
return new_history
|
|
638
|
+
|
|
489
639
|
# Otherwise, insert it after the previous_item_id if that is set
|
|
490
640
|
elif event.previous_item_id:
|
|
491
641
|
# Insert the new item after the previous item
|
|
@@ -619,12 +769,11 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
619
769
|
starting_settings: RealtimeSessionModelSettings | None,
|
|
620
770
|
agent: RealtimeAgent,
|
|
621
771
|
) -> RealtimeSessionModelSettings:
|
|
622
|
-
# Start with
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
updated_settings.update(starting_settings)
|
|
772
|
+
# Start with the merged base settings from run and model configuration.
|
|
773
|
+
updated_settings = self._base_model_settings.copy()
|
|
774
|
+
|
|
775
|
+
if agent.prompt is not None:
|
|
776
|
+
updated_settings["prompt"] = agent.prompt
|
|
628
777
|
|
|
629
778
|
instructions, tools, handoffs = await asyncio.gather(
|
|
630
779
|
agent.get_system_prompt(self._context_wrapper),
|
|
@@ -635,6 +784,10 @@ class RealtimeSession(RealtimeModelListener):
|
|
|
635
784
|
updated_settings["tools"] = tools or []
|
|
636
785
|
updated_settings["handoffs"] = handoffs or []
|
|
637
786
|
|
|
787
|
+
# Apply starting settings (from model config) next
|
|
788
|
+
if starting_settings:
|
|
789
|
+
updated_settings.update(starting_settings)
|
|
790
|
+
|
|
638
791
|
disable_tracing = self._run_config.get("tracing_disabled", False)
|
|
639
792
|
if disable_tracing:
|
|
640
793
|
updated_settings["tracing"] = None
|
agents/run.py
CHANGED
|
@@ -54,7 +54,7 @@ from .items import (
|
|
|
54
54
|
)
|
|
55
55
|
from .lifecycle import RunHooks
|
|
56
56
|
from .logger import logger
|
|
57
|
-
from .memory import Session
|
|
57
|
+
from .memory import Session, SessionInputCallback
|
|
58
58
|
from .model_settings import ModelSettings
|
|
59
59
|
from .models.interface import Model, ModelProvider
|
|
60
60
|
from .models.multi_provider import MultiProvider
|
|
@@ -179,6 +179,13 @@ class RunConfig:
|
|
|
179
179
|
An optional dictionary of additional metadata to include with the trace.
|
|
180
180
|
"""
|
|
181
181
|
|
|
182
|
+
session_input_callback: SessionInputCallback | None = None
|
|
183
|
+
"""Defines how to handle session history when new input is provided.
|
|
184
|
+
- `None` (default): The new input is appended to the session history.
|
|
185
|
+
- `SessionInputCallback`: A custom function that receives the history and new input, and
|
|
186
|
+
returns the desired combined list of items.
|
|
187
|
+
"""
|
|
188
|
+
|
|
182
189
|
call_model_input_filter: CallModelInputFilter | None = None
|
|
183
190
|
"""
|
|
184
191
|
Optional callback that is invoked immediately before calling the model. It receives the current
|
|
@@ -411,8 +418,11 @@ class AgentRunner:
|
|
|
411
418
|
if run_config is None:
|
|
412
419
|
run_config = RunConfig()
|
|
413
420
|
|
|
414
|
-
#
|
|
415
|
-
|
|
421
|
+
# Keep original user input separate from session-prepared input
|
|
422
|
+
original_user_input = input
|
|
423
|
+
prepared_input = await self._prepare_input_with_session(
|
|
424
|
+
input, session, run_config.session_input_callback
|
|
425
|
+
)
|
|
416
426
|
|
|
417
427
|
tool_use_tracker = AgentToolUseTracker()
|
|
418
428
|
|
|
@@ -438,6 +448,9 @@ class AgentRunner:
|
|
|
438
448
|
current_agent = starting_agent
|
|
439
449
|
should_run_agent_start_hooks = True
|
|
440
450
|
|
|
451
|
+
# save only the new user input to the session, not the combined history
|
|
452
|
+
await self._save_result_to_session(session, original_user_input, [])
|
|
453
|
+
|
|
441
454
|
try:
|
|
442
455
|
while True:
|
|
443
456
|
all_tools = await AgentRunner._get_all_tools(current_agent, context_wrapper)
|
|
@@ -537,9 +550,7 @@ class AgentRunner:
|
|
|
537
550
|
output_guardrail_results=output_guardrail_results,
|
|
538
551
|
context_wrapper=context_wrapper,
|
|
539
552
|
)
|
|
540
|
-
|
|
541
|
-
# Save the conversation to session if enabled
|
|
542
|
-
await self._save_result_to_session(session, input, result)
|
|
553
|
+
await self._save_result_to_session(session, [], turn_result.new_step_items)
|
|
543
554
|
|
|
544
555
|
return result
|
|
545
556
|
elif isinstance(turn_result.next_step, NextStepHandoff):
|
|
@@ -548,7 +559,7 @@ class AgentRunner:
|
|
|
548
559
|
current_span = None
|
|
549
560
|
should_run_agent_start_hooks = True
|
|
550
561
|
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
551
|
-
|
|
562
|
+
await self._save_result_to_session(session, [], turn_result.new_step_items)
|
|
552
563
|
else:
|
|
553
564
|
raise AgentsException(
|
|
554
565
|
f"Unknown next step type: {type(turn_result.next_step)}"
|
|
@@ -779,11 +790,15 @@ class AgentRunner:
|
|
|
779
790
|
|
|
780
791
|
try:
|
|
781
792
|
# Prepare input with session if enabled
|
|
782
|
-
prepared_input = await AgentRunner._prepare_input_with_session(
|
|
793
|
+
prepared_input = await AgentRunner._prepare_input_with_session(
|
|
794
|
+
starting_input, session, run_config.session_input_callback
|
|
795
|
+
)
|
|
783
796
|
|
|
784
797
|
# Update the streamed result with the prepared input
|
|
785
798
|
streamed_result.input = prepared_input
|
|
786
799
|
|
|
800
|
+
await AgentRunner._save_result_to_session(session, starting_input, [])
|
|
801
|
+
|
|
787
802
|
while True:
|
|
788
803
|
if streamed_result.is_complete:
|
|
789
804
|
break
|
|
@@ -887,24 +902,15 @@ class AgentRunner:
|
|
|
887
902
|
streamed_result.is_complete = True
|
|
888
903
|
|
|
889
904
|
# Save the conversation to session if enabled
|
|
890
|
-
# Create a temporary RunResult for session saving
|
|
891
|
-
temp_result = RunResult(
|
|
892
|
-
input=streamed_result.input,
|
|
893
|
-
new_items=streamed_result.new_items,
|
|
894
|
-
raw_responses=streamed_result.raw_responses,
|
|
895
|
-
final_output=streamed_result.final_output,
|
|
896
|
-
_last_agent=current_agent,
|
|
897
|
-
input_guardrail_results=streamed_result.input_guardrail_results,
|
|
898
|
-
output_guardrail_results=streamed_result.output_guardrail_results,
|
|
899
|
-
context_wrapper=context_wrapper,
|
|
900
|
-
)
|
|
901
905
|
await AgentRunner._save_result_to_session(
|
|
902
|
-
session,
|
|
906
|
+
session, [], turn_result.new_step_items
|
|
903
907
|
)
|
|
904
908
|
|
|
905
909
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
906
910
|
elif isinstance(turn_result.next_step, NextStepRunAgain):
|
|
907
|
-
|
|
911
|
+
await AgentRunner._save_result_to_session(
|
|
912
|
+
session, [], turn_result.new_step_items
|
|
913
|
+
)
|
|
908
914
|
except AgentsException as exc:
|
|
909
915
|
streamed_result.is_complete = True
|
|
910
916
|
streamed_result._event_queue.put_nowait(QueueCompleteSentinel())
|
|
@@ -1479,19 +1485,20 @@ class AgentRunner:
|
|
|
1479
1485
|
cls,
|
|
1480
1486
|
input: str | list[TResponseInputItem],
|
|
1481
1487
|
session: Session | None,
|
|
1488
|
+
session_input_callback: SessionInputCallback | None,
|
|
1482
1489
|
) -> str | list[TResponseInputItem]:
|
|
1483
1490
|
"""Prepare input by combining it with session history if enabled."""
|
|
1484
1491
|
if session is None:
|
|
1485
1492
|
return input
|
|
1486
1493
|
|
|
1487
|
-
#
|
|
1488
|
-
|
|
1489
|
-
if isinstance(input, list):
|
|
1494
|
+
# If the user doesn't specify an input callback and pass a list as input
|
|
1495
|
+
if isinstance(input, list) and not session_input_callback:
|
|
1490
1496
|
raise UserError(
|
|
1491
|
-
"
|
|
1492
|
-
"
|
|
1493
|
-
"
|
|
1494
|
-
"
|
|
1497
|
+
"When using session memory, list inputs require a "
|
|
1498
|
+
"`RunConfig.session_input_callback` to define how they should be merged "
|
|
1499
|
+
"with the conversation history. If you don't want to use a callback, "
|
|
1500
|
+
"provide your input as a string instead, or disable session memory "
|
|
1501
|
+
"(session=None) and pass a list to manage the history manually."
|
|
1495
1502
|
)
|
|
1496
1503
|
|
|
1497
1504
|
# Get previous conversation history
|
|
@@ -1500,19 +1507,31 @@ class AgentRunner:
|
|
|
1500
1507
|
# Convert input to list format
|
|
1501
1508
|
new_input_list = ItemHelpers.input_to_new_input_list(input)
|
|
1502
1509
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1510
|
+
if session_input_callback is None:
|
|
1511
|
+
return history + new_input_list
|
|
1512
|
+
elif callable(session_input_callback):
|
|
1513
|
+
res = session_input_callback(history, new_input_list)
|
|
1514
|
+
if inspect.isawaitable(res):
|
|
1515
|
+
return await res
|
|
1516
|
+
return res
|
|
1517
|
+
else:
|
|
1518
|
+
raise UserError(
|
|
1519
|
+
f"Invalid `session_input_callback` value: {session_input_callback}. "
|
|
1520
|
+
"Choose between `None` or a custom callable function."
|
|
1521
|
+
)
|
|
1507
1522
|
|
|
1508
1523
|
@classmethod
|
|
1509
1524
|
async def _save_result_to_session(
|
|
1510
1525
|
cls,
|
|
1511
1526
|
session: Session | None,
|
|
1512
1527
|
original_input: str | list[TResponseInputItem],
|
|
1513
|
-
|
|
1528
|
+
new_items: list[RunItem],
|
|
1514
1529
|
) -> None:
|
|
1515
|
-
"""
|
|
1530
|
+
"""
|
|
1531
|
+
Save the conversation turn to session.
|
|
1532
|
+
It does not account for any filtering or modification performed by
|
|
1533
|
+
`RunConfig.session_input_callback`.
|
|
1534
|
+
"""
|
|
1516
1535
|
if session is None:
|
|
1517
1536
|
return
|
|
1518
1537
|
|
|
@@ -1520,7 +1539,7 @@ class AgentRunner:
|
|
|
1520
1539
|
input_list = ItemHelpers.input_to_new_input_list(original_input)
|
|
1521
1540
|
|
|
1522
1541
|
# Convert new items to input format
|
|
1523
|
-
new_items_as_input = [item.to_input_item() for item in
|
|
1542
|
+
new_items_as_input = [item.to_input_item() for item in new_items]
|
|
1524
1543
|
|
|
1525
1544
|
# Save all items from this turn
|
|
1526
1545
|
items_to_save = input_list + new_items_as_input
|
agents/tool.py
CHANGED
agents/util/_json.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Any, Literal
|
|
4
5
|
|
|
5
6
|
from pydantic import TypeAdapter, ValidationError
|
|
6
7
|
from typing_extensions import TypeVar
|
|
@@ -29,3 +30,20 @@ def validate_json(json_str: str, type_adapter: TypeAdapter[T], partial: bool) ->
|
|
|
29
30
|
raise ModelBehaviorError(
|
|
30
31
|
f"Invalid JSON when parsing {json_str} for {type_adapter}; {e}"
|
|
31
32
|
) from e
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_dump_compatible(obj: Any) -> Any:
|
|
36
|
+
return _to_dump_compatible_internal(obj)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _to_dump_compatible_internal(obj: Any) -> Any:
|
|
40
|
+
if isinstance(obj, dict):
|
|
41
|
+
return {k: _to_dump_compatible_internal(v) for k, v in obj.items()}
|
|
42
|
+
|
|
43
|
+
if isinstance(obj, (list, tuple)):
|
|
44
|
+
return [_to_dump_compatible_internal(x) for x in obj]
|
|
45
|
+
|
|
46
|
+
if isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, bytearray)):
|
|
47
|
+
return [_to_dump_compatible_internal(x) for x in obj]
|
|
48
|
+
|
|
49
|
+
return obj
|
agents/voice/input.py
CHANGED
|
@@ -13,7 +13,7 @@ DEFAULT_SAMPLE_RATE = 24000
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _buffer_to_audio_file(
|
|
16
|
-
buffer: npt.NDArray[np.int16 | np.float32],
|
|
16
|
+
buffer: npt.NDArray[np.int16 | np.float32 | np.float64],
|
|
17
17
|
frame_rate: int = DEFAULT_SAMPLE_RATE,
|
|
18
18
|
sample_width: int = 2,
|
|
19
19
|
channels: int = 1,
|
|
@@ -77,12 +77,13 @@ class StreamedAudioInput:
|
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
79
|
def __init__(self):
|
|
80
|
-
self.queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32]] = asyncio.Queue()
|
|
80
|
+
self.queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32] | None] = asyncio.Queue()
|
|
81
81
|
|
|
82
|
-
async def add_audio(self, audio: npt.NDArray[np.int16 | np.float32]):
|
|
82
|
+
async def add_audio(self, audio: npt.NDArray[np.int16 | np.float32] | None):
|
|
83
83
|
"""Adds more audio data to the stream.
|
|
84
84
|
|
|
85
85
|
Args:
|
|
86
|
-
audio: The audio data to add. Must be a numpy array of int16 or float32.
|
|
86
|
+
audio: The audio data to add. Must be a numpy array of int16 or float32 or None.
|
|
87
|
+
If None passed, it indicates the end of the stream.
|
|
87
88
|
"""
|
|
88
89
|
await self.queue.put(audio)
|
|
@@ -88,7 +88,7 @@ class OpenAISTTTranscriptionSession(StreamedTranscriptionSession):
|
|
|
88
88
|
self._trace_include_sensitive_data = trace_include_sensitive_data
|
|
89
89
|
self._trace_include_sensitive_audio_data = trace_include_sensitive_audio_data
|
|
90
90
|
|
|
91
|
-
self._input_queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32]] = input.queue
|
|
91
|
+
self._input_queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32] | None] = input.queue
|
|
92
92
|
self._output_queue: asyncio.Queue[str | ErrorSentinel | SessionCompleteSentinel] = (
|
|
93
93
|
asyncio.Queue()
|
|
94
94
|
)
|
|
@@ -226,7 +226,10 @@ class OpenAISTTTranscriptionSession(StreamedTranscriptionSession):
|
|
|
226
226
|
break
|
|
227
227
|
|
|
228
228
|
event_type = event.get("type", "unknown")
|
|
229
|
-
if event_type
|
|
229
|
+
if event_type in [
|
|
230
|
+
"input_audio_transcription_completed", # legacy
|
|
231
|
+
"conversation.item.input_audio_transcription.completed",
|
|
232
|
+
]:
|
|
230
233
|
transcript = cast(str, event.get("transcript", ""))
|
|
231
234
|
if len(transcript) > 0:
|
|
232
235
|
self._end_turn(transcript)
|
|
@@ -242,7 +245,7 @@ class OpenAISTTTranscriptionSession(StreamedTranscriptionSession):
|
|
|
242
245
|
await self._output_queue.put(SessionCompleteSentinel())
|
|
243
246
|
|
|
244
247
|
async def _stream_audio(
|
|
245
|
-
self, audio_queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32]]
|
|
248
|
+
self, audio_queue: asyncio.Queue[npt.NDArray[np.int16 | np.float32] | None]
|
|
246
249
|
) -> None:
|
|
247
250
|
assert self._websocket is not None, "Websocket not initialized"
|
|
248
251
|
self._start_turn()
|
|
@@ -275,7 +278,6 @@ class OpenAISTTTranscriptionSession(StreamedTranscriptionSession):
|
|
|
275
278
|
"wss://api.openai.com/v1/realtime?intent=transcription",
|
|
276
279
|
additional_headers={
|
|
277
280
|
"Authorization": f"Bearer {self._client.api_key}",
|
|
278
|
-
"OpenAI-Beta": "realtime=v1",
|
|
279
281
|
"OpenAI-Log-Session": "1",
|
|
280
282
|
},
|
|
281
283
|
) as ws:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openai-agents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: OpenAI Agents SDK
|
|
5
5
|
Project-URL: Homepage, https://openai.github.io/openai-agents-python/
|
|
6
6
|
Project-URL: Repository, https://github.com/openai/openai-agents-python
|
|
@@ -21,7 +21,7 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Requires-Dist: griffe<2,>=1.5.6
|
|
23
23
|
Requires-Dist: mcp<2,>=1.11.0; python_version >= '3.10'
|
|
24
|
-
Requires-Dist: openai<2,>=1.
|
|
24
|
+
Requires-Dist: openai<2,>=1.107.1
|
|
25
25
|
Requires-Dist: pydantic<3,>=2.10
|
|
26
26
|
Requires-Dist: requests<3,>=2.0
|
|
27
27
|
Requires-Dist: types-requests<3,>=2.0
|