openai-agents 0.2.10__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 +17 -4
- 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.py +6 -0
- agents/realtime/model_inputs.py +15 -1
- agents/realtime/openai_realtime.py +428 -139
- agents/realtime/session.py +167 -14
- agents/run.py +102 -54
- agents/tool.py +2 -2
- agents/util/_json.py +19 -1
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +6 -4
- {openai_agents-0.2.10.dist-info → openai_agents-0.3.0.dist-info}/METADATA +2 -2
- {openai_agents-0.2.10.dist-info → openai_agents-0.3.0.dist-info}/RECORD +26 -24
- {openai_agents-0.2.10.dist-info → openai_agents-0.3.0.dist-info}/WHEEL +0 -0
- {openai_agents-0.2.10.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
|
@@ -8,7 +8,7 @@ from typing import Any, Callable, Generic, cast, get_args
|
|
|
8
8
|
|
|
9
9
|
from openai.types.responses import (
|
|
10
10
|
ResponseCompletedEvent,
|
|
11
|
-
|
|
11
|
+
ResponseOutputItemDoneEvent,
|
|
12
12
|
)
|
|
13
13
|
from openai.types.responses.response_prompt_param import (
|
|
14
14
|
ResponsePromptParam,
|
|
@@ -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())
|
|
@@ -994,10 +1000,16 @@ class AgentRunner:
|
|
|
994
1000
|
)
|
|
995
1001
|
|
|
996
1002
|
# Call hook just before the model is invoked, with the correct system_prompt.
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1003
|
+
await asyncio.gather(
|
|
1004
|
+
hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
|
|
1005
|
+
(
|
|
1006
|
+
agent.hooks.on_llm_start(
|
|
1007
|
+
context_wrapper, agent, filtered.instructions, filtered.input
|
|
1008
|
+
)
|
|
1009
|
+
if agent.hooks
|
|
1010
|
+
else _coro.noop_coroutine()
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1001
1013
|
|
|
1002
1014
|
# 1. Stream the output events
|
|
1003
1015
|
async for event in model.stream_response(
|
|
@@ -1034,7 +1046,7 @@ class AgentRunner:
|
|
|
1034
1046
|
)
|
|
1035
1047
|
context_wrapper.usage.add(usage)
|
|
1036
1048
|
|
|
1037
|
-
if isinstance(event,
|
|
1049
|
+
if isinstance(event, ResponseOutputItemDoneEvent):
|
|
1038
1050
|
output_item = event.item
|
|
1039
1051
|
|
|
1040
1052
|
if isinstance(output_item, _TOOL_CALL_TYPES):
|
|
@@ -1056,8 +1068,15 @@ class AgentRunner:
|
|
|
1056
1068
|
streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event))
|
|
1057
1069
|
|
|
1058
1070
|
# Call hook just after the model response is finalized.
|
|
1059
|
-
if
|
|
1060
|
-
await
|
|
1071
|
+
if final_response is not None:
|
|
1072
|
+
await asyncio.gather(
|
|
1073
|
+
(
|
|
1074
|
+
agent.hooks.on_llm_end(context_wrapper, agent, final_response)
|
|
1075
|
+
if agent.hooks
|
|
1076
|
+
else _coro.noop_coroutine()
|
|
1077
|
+
),
|
|
1078
|
+
hooks.on_llm_end(context_wrapper, agent, final_response),
|
|
1079
|
+
)
|
|
1061
1080
|
|
|
1062
1081
|
# 2. At this point, the streaming is complete for this turn of the agent loop.
|
|
1063
1082
|
if not final_response:
|
|
@@ -1150,6 +1169,7 @@ class AgentRunner:
|
|
|
1150
1169
|
output_schema,
|
|
1151
1170
|
all_tools,
|
|
1152
1171
|
handoffs,
|
|
1172
|
+
hooks,
|
|
1153
1173
|
context_wrapper,
|
|
1154
1174
|
run_config,
|
|
1155
1175
|
tool_use_tracker,
|
|
@@ -1345,6 +1365,7 @@ class AgentRunner:
|
|
|
1345
1365
|
output_schema: AgentOutputSchemaBase | None,
|
|
1346
1366
|
all_tools: list[Tool],
|
|
1347
1367
|
handoffs: list[Handoff],
|
|
1368
|
+
hooks: RunHooks[TContext],
|
|
1348
1369
|
context_wrapper: RunContextWrapper[TContext],
|
|
1349
1370
|
run_config: RunConfig,
|
|
1350
1371
|
tool_use_tracker: AgentToolUseTracker,
|
|
@@ -1364,14 +1385,21 @@ class AgentRunner:
|
|
|
1364
1385
|
model = cls._get_model(agent, run_config)
|
|
1365
1386
|
model_settings = agent.model_settings.resolve(run_config.model_settings)
|
|
1366
1387
|
model_settings = RunImpl.maybe_reset_tool_choice(agent, tool_use_tracker, model_settings)
|
|
1367
|
-
|
|
1368
|
-
if agent
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1388
|
+
|
|
1389
|
+
# If we have run hooks, or if the agent has hooks, we need to call them before the LLM call
|
|
1390
|
+
await asyncio.gather(
|
|
1391
|
+
hooks.on_llm_start(context_wrapper, agent, filtered.instructions, filtered.input),
|
|
1392
|
+
(
|
|
1393
|
+
agent.hooks.on_llm_start(
|
|
1394
|
+
context_wrapper,
|
|
1395
|
+
agent,
|
|
1396
|
+
filtered.instructions, # Use filtered instructions
|
|
1397
|
+
filtered.input, # Use filtered input
|
|
1398
|
+
)
|
|
1399
|
+
if agent.hooks
|
|
1400
|
+
else _coro.noop_coroutine()
|
|
1401
|
+
),
|
|
1402
|
+
)
|
|
1375
1403
|
|
|
1376
1404
|
new_response = await model.get_response(
|
|
1377
1405
|
system_instructions=filtered.instructions,
|
|
@@ -1387,12 +1415,19 @@ class AgentRunner:
|
|
|
1387
1415
|
conversation_id=conversation_id,
|
|
1388
1416
|
prompt=prompt_config,
|
|
1389
1417
|
)
|
|
1390
|
-
# If the agent has hooks, we need to call them after the LLM call
|
|
1391
|
-
if agent.hooks:
|
|
1392
|
-
await agent.hooks.on_llm_end(context_wrapper, agent, new_response)
|
|
1393
1418
|
|
|
1394
1419
|
context_wrapper.usage.add(new_response.usage)
|
|
1395
1420
|
|
|
1421
|
+
# If we have run hooks, or if the agent has hooks, we need to call them after the LLM call
|
|
1422
|
+
await asyncio.gather(
|
|
1423
|
+
(
|
|
1424
|
+
agent.hooks.on_llm_end(context_wrapper, agent, new_response)
|
|
1425
|
+
if agent.hooks
|
|
1426
|
+
else _coro.noop_coroutine()
|
|
1427
|
+
),
|
|
1428
|
+
hooks.on_llm_end(context_wrapper, agent, new_response),
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1396
1431
|
return new_response
|
|
1397
1432
|
|
|
1398
1433
|
@classmethod
|
|
@@ -1450,19 +1485,20 @@ class AgentRunner:
|
|
|
1450
1485
|
cls,
|
|
1451
1486
|
input: str | list[TResponseInputItem],
|
|
1452
1487
|
session: Session | None,
|
|
1488
|
+
session_input_callback: SessionInputCallback | None,
|
|
1453
1489
|
) -> str | list[TResponseInputItem]:
|
|
1454
1490
|
"""Prepare input by combining it with session history if enabled."""
|
|
1455
1491
|
if session is None:
|
|
1456
1492
|
return input
|
|
1457
1493
|
|
|
1458
|
-
#
|
|
1459
|
-
|
|
1460
|
-
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:
|
|
1461
1496
|
raise UserError(
|
|
1462
|
-
"
|
|
1463
|
-
"
|
|
1464
|
-
"
|
|
1465
|
-
"
|
|
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."
|
|
1466
1502
|
)
|
|
1467
1503
|
|
|
1468
1504
|
# Get previous conversation history
|
|
@@ -1471,19 +1507,31 @@ class AgentRunner:
|
|
|
1471
1507
|
# Convert input to list format
|
|
1472
1508
|
new_input_list = ItemHelpers.input_to_new_input_list(input)
|
|
1473
1509
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
+
)
|
|
1478
1522
|
|
|
1479
1523
|
@classmethod
|
|
1480
1524
|
async def _save_result_to_session(
|
|
1481
1525
|
cls,
|
|
1482
1526
|
session: Session | None,
|
|
1483
1527
|
original_input: str | list[TResponseInputItem],
|
|
1484
|
-
|
|
1528
|
+
new_items: list[RunItem],
|
|
1485
1529
|
) -> None:
|
|
1486
|
-
"""
|
|
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
|
+
"""
|
|
1487
1535
|
if session is None:
|
|
1488
1536
|
return
|
|
1489
1537
|
|
|
@@ -1491,7 +1539,7 @@ class AgentRunner:
|
|
|
1491
1539
|
input_list = ItemHelpers.input_to_new_input_list(original_input)
|
|
1492
1540
|
|
|
1493
1541
|
# Convert new items to input format
|
|
1494
|
-
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]
|
|
1495
1543
|
|
|
1496
1544
|
# Save all items from this turn
|
|
1497
1545
|
items_to_save = input_list + new_items_as_input
|
agents/tool.py
CHANGED
|
@@ -12,8 +12,8 @@ from openai.types.responses.response_computer_tool_call import (
|
|
|
12
12
|
ResponseComputerToolCall,
|
|
13
13
|
)
|
|
14
14
|
from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
|
|
15
|
-
from openai.types.responses.tool import WebSearchToolFilters
|
|
16
15
|
from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
|
|
16
|
+
from openai.types.responses.web_search_tool import Filters as WebSearchToolFilters
|
|
17
17
|
from openai.types.responses.web_search_tool_param import UserLocation
|
|
18
18
|
from pydantic import ValidationError
|
|
19
19
|
from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict
|
|
@@ -142,7 +142,7 @@ class WebSearchTool:
|
|
|
142
142
|
|
|
143
143
|
@property
|
|
144
144
|
def name(self):
|
|
145
|
-
return "
|
|
145
|
+
return "web_search"
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
@dataclass
|
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)
|