openai-agents 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openai-agents might be problematic. Click here for more details.

agents/__init__.py CHANGED
@@ -21,6 +21,8 @@ from .exceptions import (
21
21
  ModelBehaviorError,
22
22
  OutputGuardrailTripwireTriggered,
23
23
  RunErrorDetails,
24
+ ToolInputGuardrailTripwireTriggered,
25
+ ToolOutputGuardrailTripwireTriggered,
24
26
  UserError,
25
27
  )
26
28
  from .guardrail import (
@@ -79,10 +81,27 @@ from .tool import (
79
81
  MCPToolApprovalFunctionResult,
80
82
  MCPToolApprovalRequest,
81
83
  Tool,
84
+ ToolOutputFileContent,
85
+ ToolOutputFileContentDict,
86
+ ToolOutputImage,
87
+ ToolOutputImageDict,
88
+ ToolOutputText,
89
+ ToolOutputTextDict,
82
90
  WebSearchTool,
83
91
  default_tool_error_function,
84
92
  function_tool,
85
93
  )
94
+ from .tool_guardrails import (
95
+ ToolGuardrailFunctionOutput,
96
+ ToolInputGuardrail,
97
+ ToolInputGuardrailData,
98
+ ToolInputGuardrailResult,
99
+ ToolOutputGuardrail,
100
+ ToolOutputGuardrailData,
101
+ ToolOutputGuardrailResult,
102
+ tool_input_guardrail,
103
+ tool_output_guardrail,
104
+ )
86
105
  from .tracing import (
87
106
  AgentSpanData,
88
107
  CustomSpanData,
@@ -125,7 +144,7 @@ from .version import __version__
125
144
 
126
145
 
127
146
  def set_default_openai_key(key: str, use_for_tracing: bool = True) -> None:
128
- """Set the default OpenAI API key to use for LLM requests (and optionally tracing(). This is
147
+ """Set the default OpenAI API key to use for LLM requests (and optionally tracing()). This is
129
148
  only necessary if the OPENAI_API_KEY environment variable is not already set.
130
149
 
131
150
  If provided, this key will be used instead of the OPENAI_API_KEY environment variable.
@@ -191,6 +210,8 @@ __all__ = [
191
210
  "AgentsException",
192
211
  "InputGuardrailTripwireTriggered",
193
212
  "OutputGuardrailTripwireTriggered",
213
+ "ToolInputGuardrailTripwireTriggered",
214
+ "ToolOutputGuardrailTripwireTriggered",
194
215
  "DynamicPromptFunction",
195
216
  "GenerateDynamicPromptData",
196
217
  "Prompt",
@@ -204,6 +225,15 @@ __all__ = [
204
225
  "GuardrailFunctionOutput",
205
226
  "input_guardrail",
206
227
  "output_guardrail",
228
+ "ToolInputGuardrail",
229
+ "ToolOutputGuardrail",
230
+ "ToolGuardrailFunctionOutput",
231
+ "ToolInputGuardrailData",
232
+ "ToolInputGuardrailResult",
233
+ "ToolOutputGuardrailData",
234
+ "ToolOutputGuardrailResult",
235
+ "tool_input_guardrail",
236
+ "tool_output_guardrail",
207
237
  "handoff",
208
238
  "Handoff",
209
239
  "HandoffInputData",
@@ -249,6 +279,12 @@ __all__ = [
249
279
  "MCPToolApprovalFunction",
250
280
  "MCPToolApprovalRequest",
251
281
  "MCPToolApprovalFunctionResult",
282
+ "ToolOutputText",
283
+ "ToolOutputTextDict",
284
+ "ToolOutputImage",
285
+ "ToolOutputImageDict",
286
+ "ToolOutputFileContent",
287
+ "ToolOutputFileContentDict",
252
288
  "function_tool",
253
289
  "Usage",
254
290
  "add_trace_processor",
agents/_run_impl.py CHANGED
@@ -44,7 +44,13 @@ from openai.types.responses.response_reasoning_item import ResponseReasoningItem
44
44
  from .agent import Agent, ToolsToFinalOutputResult
45
45
  from .agent_output import AgentOutputSchemaBase
46
46
  from .computer import AsyncComputer, Computer
47
- from .exceptions import AgentsException, ModelBehaviorError, UserError
47
+ from .exceptions import (
48
+ AgentsException,
49
+ ModelBehaviorError,
50
+ ToolInputGuardrailTripwireTriggered,
51
+ ToolOutputGuardrailTripwireTriggered,
52
+ UserError,
53
+ )
48
54
  from .guardrail import InputGuardrail, InputGuardrailResult, OutputGuardrail, OutputGuardrailResult
49
55
  from .handoffs import Handoff, HandoffInputData
50
56
  from .items import (
@@ -80,6 +86,12 @@ from .tool import (
80
86
  Tool,
81
87
  )
82
88
  from .tool_context import ToolContext
89
+ from .tool_guardrails import (
90
+ ToolInputGuardrailData,
91
+ ToolInputGuardrailResult,
92
+ ToolOutputGuardrailData,
93
+ ToolOutputGuardrailResult,
94
+ )
83
95
  from .tracing import (
84
96
  SpanError,
85
97
  Trace,
@@ -208,6 +220,12 @@ class SingleStepResult:
208
220
  next_step: NextStepHandoff | NextStepFinalOutput | NextStepRunAgain
209
221
  """The next step to take."""
210
222
 
223
+ tool_input_guardrail_results: list[ToolInputGuardrailResult]
224
+ """Tool input guardrail results from this step."""
225
+
226
+ tool_output_guardrail_results: list[ToolOutputGuardrailResult]
227
+ """Tool output guardrail results from this step."""
228
+
211
229
  @property
212
230
  def generated_items(self) -> list[RunItem]:
213
231
  """Items generated during the agent run (i.e. everything generated after
@@ -249,8 +267,12 @@ class RunImpl:
249
267
  new_step_items: list[RunItem] = []
250
268
  new_step_items.extend(processed_response.new_items)
251
269
 
252
- # First, lets run the tool calls - function tools and computer actions
253
- function_results, computer_results = await asyncio.gather(
270
+ # First, lets run the tool calls - function tools, computer actions, and local shell calls
271
+ (
272
+ (function_results, tool_input_guardrail_results, tool_output_guardrail_results),
273
+ computer_results,
274
+ local_shell_results,
275
+ ) = await asyncio.gather(
254
276
  cls.execute_function_tool_calls(
255
277
  agent=agent,
256
278
  tool_runs=processed_response.functions,
@@ -265,9 +287,17 @@ class RunImpl:
265
287
  context_wrapper=context_wrapper,
266
288
  config=run_config,
267
289
  ),
290
+ cls.execute_local_shell_calls(
291
+ agent=agent,
292
+ calls=processed_response.local_shell_calls,
293
+ hooks=hooks,
294
+ context_wrapper=context_wrapper,
295
+ config=run_config,
296
+ ),
268
297
  )
269
298
  new_step_items.extend([result.run_item for result in function_results])
270
299
  new_step_items.extend(computer_results)
300
+ new_step_items.extend(local_shell_results)
271
301
 
272
302
  # Next, run the MCP approval requests
273
303
  if processed_response.mcp_approval_requests:
@@ -320,6 +350,8 @@ class RunImpl:
320
350
  final_output=check_tool_use.final_output,
321
351
  hooks=hooks,
322
352
  context_wrapper=context_wrapper,
353
+ tool_input_guardrail_results=tool_input_guardrail_results,
354
+ tool_output_guardrail_results=tool_output_guardrail_results,
323
355
  )
324
356
 
325
357
  # Now we can check if the model also produced a final output
@@ -343,6 +375,8 @@ class RunImpl:
343
375
  final_output=final_output,
344
376
  hooks=hooks,
345
377
  context_wrapper=context_wrapper,
378
+ tool_input_guardrail_results=tool_input_guardrail_results,
379
+ tool_output_guardrail_results=tool_output_guardrail_results,
346
380
  )
347
381
  elif not output_schema or output_schema.is_plain_text():
348
382
  return await cls.execute_final_output(
@@ -354,6 +388,8 @@ class RunImpl:
354
388
  final_output=potential_final_output_text or "",
355
389
  hooks=hooks,
356
390
  context_wrapper=context_wrapper,
391
+ tool_input_guardrail_results=tool_input_guardrail_results,
392
+ tool_output_guardrail_results=tool_output_guardrail_results,
357
393
  )
358
394
 
359
395
  # If there's no final output, we can just run again
@@ -363,6 +399,8 @@ class RunImpl:
363
399
  pre_step_items=pre_step_items,
364
400
  new_step_items=new_step_items,
365
401
  next_step=NextStepRunAgain(),
402
+ tool_input_guardrail_results=tool_input_guardrail_results,
403
+ tool_output_guardrail_results=tool_output_guardrail_results,
366
404
  )
367
405
 
368
406
  @classmethod
@@ -547,6 +585,155 @@ class RunImpl:
547
585
  mcp_approval_requests=mcp_approval_requests,
548
586
  )
549
587
 
588
+ @classmethod
589
+ async def _execute_input_guardrails(
590
+ cls,
591
+ *,
592
+ func_tool: FunctionTool,
593
+ tool_context: ToolContext[TContext],
594
+ agent: Agent[TContext],
595
+ tool_input_guardrail_results: list[ToolInputGuardrailResult],
596
+ ) -> str | None:
597
+ """Execute input guardrails for a tool.
598
+
599
+ Args:
600
+ func_tool: The function tool being executed.
601
+ tool_context: The tool execution context.
602
+ agent: The agent executing the tool.
603
+ tool_input_guardrail_results: List to append guardrail results to.
604
+
605
+ Returns:
606
+ None if tool execution should proceed, or a message string if execution should be
607
+ skipped.
608
+
609
+ Raises:
610
+ ToolInputGuardrailTripwireTriggered: If a guardrail triggers an exception.
611
+ """
612
+ if not func_tool.tool_input_guardrails:
613
+ return None
614
+
615
+ for guardrail in func_tool.tool_input_guardrails:
616
+ gr_out = await guardrail.run(
617
+ ToolInputGuardrailData(
618
+ context=tool_context,
619
+ agent=agent,
620
+ )
621
+ )
622
+
623
+ # Store the guardrail result
624
+ tool_input_guardrail_results.append(
625
+ ToolInputGuardrailResult(
626
+ guardrail=guardrail,
627
+ output=gr_out,
628
+ )
629
+ )
630
+
631
+ # Handle different behavior types
632
+ if gr_out.behavior["type"] == "raise_exception":
633
+ raise ToolInputGuardrailTripwireTriggered(guardrail=guardrail, output=gr_out)
634
+ elif gr_out.behavior["type"] == "reject_content":
635
+ # Set final_result to the message and skip tool execution
636
+ return gr_out.behavior["message"]
637
+ elif gr_out.behavior["type"] == "allow":
638
+ # Continue to next guardrail or tool execution
639
+ continue
640
+
641
+ return None
642
+
643
+ @classmethod
644
+ async def _execute_output_guardrails(
645
+ cls,
646
+ *,
647
+ func_tool: FunctionTool,
648
+ tool_context: ToolContext[TContext],
649
+ agent: Agent[TContext],
650
+ real_result: Any,
651
+ tool_output_guardrail_results: list[ToolOutputGuardrailResult],
652
+ ) -> Any:
653
+ """Execute output guardrails for a tool.
654
+
655
+ Args:
656
+ func_tool: The function tool being executed.
657
+ tool_context: The tool execution context.
658
+ agent: The agent executing the tool.
659
+ real_result: The actual result from the tool execution.
660
+ tool_output_guardrail_results: List to append guardrail results to.
661
+
662
+ Returns:
663
+ The final result after guardrail processing (may be modified).
664
+
665
+ Raises:
666
+ ToolOutputGuardrailTripwireTriggered: If a guardrail triggers an exception.
667
+ """
668
+ if not func_tool.tool_output_guardrails:
669
+ return real_result
670
+
671
+ final_result = real_result
672
+ for output_guardrail in func_tool.tool_output_guardrails:
673
+ gr_out = await output_guardrail.run(
674
+ ToolOutputGuardrailData(
675
+ context=tool_context,
676
+ agent=agent,
677
+ output=real_result,
678
+ )
679
+ )
680
+
681
+ # Store the guardrail result
682
+ tool_output_guardrail_results.append(
683
+ ToolOutputGuardrailResult(
684
+ guardrail=output_guardrail,
685
+ output=gr_out,
686
+ )
687
+ )
688
+
689
+ # Handle different behavior types
690
+ if gr_out.behavior["type"] == "raise_exception":
691
+ raise ToolOutputGuardrailTripwireTriggered(
692
+ guardrail=output_guardrail, output=gr_out
693
+ )
694
+ elif gr_out.behavior["type"] == "reject_content":
695
+ # Override the result with the guardrail message
696
+ final_result = gr_out.behavior["message"]
697
+ break
698
+ elif gr_out.behavior["type"] == "allow":
699
+ # Continue to next guardrail
700
+ continue
701
+
702
+ return final_result
703
+
704
+ @classmethod
705
+ async def _execute_tool_with_hooks(
706
+ cls,
707
+ *,
708
+ func_tool: FunctionTool,
709
+ tool_context: ToolContext[TContext],
710
+ agent: Agent[TContext],
711
+ hooks: RunHooks[TContext],
712
+ tool_call: ResponseFunctionToolCall,
713
+ ) -> Any:
714
+ """Execute the core tool function with before/after hooks.
715
+
716
+ Args:
717
+ func_tool: The function tool being executed.
718
+ tool_context: The tool execution context.
719
+ agent: The agent executing the tool.
720
+ hooks: The run hooks to execute.
721
+ tool_call: The tool call details.
722
+
723
+ Returns:
724
+ The result from the tool execution.
725
+ """
726
+ await asyncio.gather(
727
+ hooks.on_tool_start(tool_context, agent, func_tool),
728
+ (
729
+ agent.hooks.on_tool_start(tool_context, agent, func_tool)
730
+ if agent.hooks
731
+ else _coro.noop_coroutine()
732
+ ),
733
+ )
734
+
735
+ return await func_tool.on_invoke_tool(tool_context, tool_call.arguments)
736
+
550
737
  @classmethod
551
738
  async def execute_function_tool_calls(
552
739
  cls,
@@ -556,7 +743,13 @@ class RunImpl:
556
743
  hooks: RunHooks[TContext],
557
744
  context_wrapper: RunContextWrapper[TContext],
558
745
  config: RunConfig,
559
- ) -> list[FunctionToolResult]:
746
+ ) -> tuple[
747
+ list[FunctionToolResult], list[ToolInputGuardrailResult], list[ToolOutputGuardrailResult]
748
+ ]:
749
+ # Collect guardrail results
750
+ tool_input_guardrail_results: list[ToolInputGuardrailResult] = []
751
+ tool_output_guardrail_results: list[ToolOutputGuardrailResult] = []
752
+
560
753
  async def run_single_tool(
561
754
  func_tool: FunctionTool, tool_call: ResponseFunctionToolCall
562
755
  ) -> Any:
@@ -569,24 +762,48 @@ class RunImpl:
569
762
  if config.trace_include_sensitive_data:
570
763
  span_fn.span_data.input = tool_call.arguments
571
764
  try:
572
- _, _, result = await asyncio.gather(
573
- hooks.on_tool_start(tool_context, agent, func_tool),
574
- (
575
- agent.hooks.on_tool_start(tool_context, agent, func_tool)
576
- if agent.hooks
577
- else _coro.noop_coroutine()
578
- ),
579
- func_tool.on_invoke_tool(tool_context, tool_call.arguments),
765
+ # 1) Run input tool guardrails, if any
766
+ rejected_message = await cls._execute_input_guardrails(
767
+ func_tool=func_tool,
768
+ tool_context=tool_context,
769
+ agent=agent,
770
+ tool_input_guardrail_results=tool_input_guardrail_results,
580
771
  )
581
772
 
582
- await asyncio.gather(
583
- hooks.on_tool_end(tool_context, agent, func_tool, result),
584
- (
585
- agent.hooks.on_tool_end(tool_context, agent, func_tool, result)
586
- if agent.hooks
587
- else _coro.noop_coroutine()
588
- ),
589
- )
773
+ if rejected_message is not None:
774
+ # Input guardrail rejected the tool call
775
+ final_result = rejected_message
776
+ else:
777
+ # 2) Actually run the tool
778
+ real_result = await cls._execute_tool_with_hooks(
779
+ func_tool=func_tool,
780
+ tool_context=tool_context,
781
+ agent=agent,
782
+ hooks=hooks,
783
+ tool_call=tool_call,
784
+ )
785
+
786
+ # 3) Run output tool guardrails, if any
787
+ final_result = await cls._execute_output_guardrails(
788
+ func_tool=func_tool,
789
+ tool_context=tool_context,
790
+ agent=agent,
791
+ real_result=real_result,
792
+ tool_output_guardrail_results=tool_output_guardrail_results,
793
+ )
794
+
795
+ # 4) Tool end hooks (with final result, which may have been overridden)
796
+ await asyncio.gather(
797
+ hooks.on_tool_end(tool_context, agent, func_tool, final_result),
798
+ (
799
+ agent.hooks.on_tool_end(
800
+ tool_context, agent, func_tool, final_result
801
+ )
802
+ if agent.hooks
803
+ else _coro.noop_coroutine()
804
+ ),
805
+ )
806
+ result = final_result
590
807
  except Exception as e:
591
808
  _error_tracing.attach_error_to_current_span(
592
809
  SpanError(
@@ -609,19 +826,21 @@ class RunImpl:
609
826
 
610
827
  results = await asyncio.gather(*tasks)
611
828
 
612
- return [
829
+ function_tool_results = [
613
830
  FunctionToolResult(
614
831
  tool=tool_run.function_tool,
615
832
  output=result,
616
833
  run_item=ToolCallOutputItem(
617
834
  output=result,
618
- raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, str(result)),
835
+ raw_item=ItemHelpers.tool_call_output_item(tool_run.tool_call, result),
619
836
  agent=agent,
620
837
  ),
621
838
  )
622
839
  for tool_run, result in zip(tool_runs, results)
623
840
  ]
624
841
 
842
+ return function_tool_results, tool_input_guardrail_results, tool_output_guardrail_results
843
+
625
844
  @classmethod
626
845
  async def execute_local_shell_calls(
627
846
  cls,
@@ -825,6 +1044,8 @@ class RunImpl:
825
1044
  pre_step_items=pre_step_items,
826
1045
  new_step_items=new_step_items,
827
1046
  next_step=NextStepHandoff(new_agent),
1047
+ tool_input_guardrail_results=[],
1048
+ tool_output_guardrail_results=[],
828
1049
  )
829
1050
 
830
1051
  @classmethod
@@ -873,6 +1094,8 @@ class RunImpl:
873
1094
  final_output: Any,
874
1095
  hooks: RunHooks[TContext],
875
1096
  context_wrapper: RunContextWrapper[TContext],
1097
+ tool_input_guardrail_results: list[ToolInputGuardrailResult],
1098
+ tool_output_guardrail_results: list[ToolOutputGuardrailResult],
876
1099
  ) -> SingleStepResult:
877
1100
  # Run the on_end hooks
878
1101
  await cls.run_final_output_hooks(agent, hooks, context_wrapper, final_output)
@@ -883,6 +1106,8 @@ class RunImpl:
883
1106
  pre_step_items=pre_step_items,
884
1107
  new_step_items=new_step_items,
885
1108
  next_step=NextStepFinalOutput(final_output),
1109
+ tool_input_guardrail_results=tool_input_guardrail_results,
1110
+ tool_output_guardrail_results=tool_output_guardrail_results,
886
1111
  )
887
1112
 
888
1113
  @classmethod
@@ -1198,12 +1423,13 @@ class LocalShellAction:
1198
1423
 
1199
1424
  return ToolCallOutputItem(
1200
1425
  agent=agent,
1201
- output=output,
1202
- raw_item={
1426
+ output=result,
1427
+ # LocalShellCallOutput type uses the field name "id", but the server wants "call_id".
1428
+ # raw_item keeps the upstream type, so we ignore the type checker here.
1429
+ raw_item={ # type: ignore[misc, arg-type]
1203
1430
  "type": "local_shell_call_output",
1204
- "id": call.tool_call.call_id,
1431
+ "call_id": call.tool_call.call_id,
1205
1432
  "output": result,
1206
- # "id": "out" + call.tool_call.id, # TODO remove this, it should be optional
1207
1433
  },
1208
1434
  )
1209
1435
 
agents/exceptions.py CHANGED
@@ -8,6 +8,11 @@ if TYPE_CHECKING:
8
8
  from .guardrail import InputGuardrailResult, OutputGuardrailResult
9
9
  from .items import ModelResponse, RunItem, TResponseInputItem
10
10
  from .run_context import RunContextWrapper
11
+ from .tool_guardrails import (
12
+ ToolGuardrailFunctionOutput,
13
+ ToolInputGuardrail,
14
+ ToolOutputGuardrail,
15
+ )
11
16
 
12
17
  from .util._pretty_print import pretty_print_run_error_details
13
18
 
@@ -94,3 +99,33 @@ class OutputGuardrailTripwireTriggered(AgentsException):
94
99
  super().__init__(
95
100
  f"Guardrail {guardrail_result.guardrail.__class__.__name__} triggered tripwire"
96
101
  )
102
+
103
+
104
+ class ToolInputGuardrailTripwireTriggered(AgentsException):
105
+ """Exception raised when a tool input guardrail tripwire is triggered."""
106
+
107
+ guardrail: ToolInputGuardrail[Any]
108
+ """The guardrail that was triggered."""
109
+
110
+ output: ToolGuardrailFunctionOutput
111
+ """The output from the guardrail function."""
112
+
113
+ def __init__(self, guardrail: ToolInputGuardrail[Any], output: ToolGuardrailFunctionOutput):
114
+ self.guardrail = guardrail
115
+ self.output = output
116
+ super().__init__(f"Tool input guardrail {guardrail.__class__.__name__} triggered tripwire")
117
+
118
+
119
+ class ToolOutputGuardrailTripwireTriggered(AgentsException):
120
+ """Exception raised when a tool output guardrail tripwire is triggered."""
121
+
122
+ guardrail: ToolOutputGuardrail[Any]
123
+ """The guardrail that was triggered."""
124
+
125
+ output: ToolGuardrailFunctionOutput
126
+ """The output from the guardrail function."""
127
+
128
+ def __init__(self, guardrail: ToolOutputGuardrail[Any], output: ToolGuardrailFunctionOutput):
129
+ self.guardrail = guardrail
130
+ self.output = output
131
+ super().__init__(f"Tool output guardrail {guardrail.__class__.__name__} triggered tripwire")
@@ -12,7 +12,9 @@ from typing import Any
12
12
 
13
13
  __all__: list[str] = [
14
14
  "EncryptedSession",
15
+ "RedisSession",
15
16
  "SQLAlchemySession",
17
+ "AdvancedSQLiteSession",
16
18
  ]
17
19
 
18
20
 
@@ -28,6 +30,17 @@ def __getattr__(name: str) -> Any:
28
30
  "Install it with: pip install openai-agents[encrypt]"
29
31
  ) from e
30
32
 
33
+ if name == "RedisSession":
34
+ try:
35
+ from .redis_session import RedisSession # noqa: F401
36
+
37
+ return RedisSession
38
+ except ModuleNotFoundError as e:
39
+ raise ImportError(
40
+ "RedisSession requires the 'redis' extra. "
41
+ "Install it with: pip install openai-agents[redis]"
42
+ ) from e
43
+
31
44
  if name == "SQLAlchemySession":
32
45
  try:
33
46
  from .sqlalchemy_session import SQLAlchemySession # noqa: F401
@@ -39,4 +52,12 @@ def __getattr__(name: str) -> Any:
39
52
  "Install it with: pip install openai-agents[sqlalchemy]"
40
53
  ) from e
41
54
 
55
+ if name == "AdvancedSQLiteSession":
56
+ try:
57
+ from .advanced_sqlite_session import AdvancedSQLiteSession # noqa: F401
58
+
59
+ return AdvancedSQLiteSession
60
+ except ModuleNotFoundError as e:
61
+ raise ImportError(f"Failed to import AdvancedSQLiteSession: {e}") from e
62
+
42
63
  raise AttributeError(f"module {__name__} has no attribute {name}")