openai-agents 0.2.8__py3-none-any.whl → 0.6.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +105 -4
- agents/_debug.py +15 -4
- agents/_run_impl.py +1203 -96
- agents/agent.py +164 -19
- agents/apply_diff.py +329 -0
- agents/editor.py +47 -0
- agents/exceptions.py +35 -0
- agents/extensions/experimental/__init__.py +6 -0
- agents/extensions/experimental/codex/__init__.py +92 -0
- agents/extensions/experimental/codex/codex.py +89 -0
- agents/extensions/experimental/codex/codex_options.py +35 -0
- agents/extensions/experimental/codex/codex_tool.py +1142 -0
- agents/extensions/experimental/codex/events.py +162 -0
- agents/extensions/experimental/codex/exec.py +263 -0
- agents/extensions/experimental/codex/items.py +245 -0
- agents/extensions/experimental/codex/output_schema_file.py +50 -0
- agents/extensions/experimental/codex/payloads.py +31 -0
- agents/extensions/experimental/codex/thread.py +214 -0
- agents/extensions/experimental/codex/thread_options.py +54 -0
- agents/extensions/experimental/codex/turn_options.py +36 -0
- agents/extensions/handoff_filters.py +13 -1
- agents/extensions/memory/__init__.py +120 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/async_sqlite_session.py +239 -0
- agents/extensions/memory/dapr_session.py +423 -0
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/memory/redis_session.py +261 -0
- agents/extensions/memory/sqlalchemy_session.py +334 -0
- agents/extensions/models/litellm_model.py +449 -36
- agents/extensions/models/litellm_provider.py +3 -1
- agents/function_schema.py +47 -5
- agents/guardrail.py +16 -2
- agents/{handoffs.py → handoffs/__init__.py} +89 -47
- agents/handoffs/history.py +268 -0
- agents/items.py +237 -11
- agents/lifecycle.py +75 -14
- agents/mcp/server.py +280 -37
- agents/mcp/util.py +24 -3
- agents/memory/__init__.py +22 -2
- agents/memory/openai_conversations_session.py +91 -0
- agents/memory/openai_responses_compaction_session.py +249 -0
- agents/memory/session.py +19 -261
- agents/memory/sqlite_session.py +275 -0
- agents/memory/util.py +20 -0
- agents/model_settings.py +14 -3
- agents/models/__init__.py +13 -0
- agents/models/chatcmpl_converter.py +303 -50
- agents/models/chatcmpl_helpers.py +63 -0
- agents/models/chatcmpl_stream_handler.py +290 -68
- agents/models/default_models.py +58 -0
- agents/models/interface.py +4 -0
- agents/models/openai_chatcompletions.py +103 -49
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +162 -46
- agents/realtime/__init__.py +4 -0
- agents/realtime/_util.py +14 -3
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +53 -0
- agents/realtime/config.py +78 -10
- agents/realtime/events.py +18 -0
- agents/realtime/handoffs.py +2 -2
- agents/realtime/items.py +17 -1
- agents/realtime/model.py +13 -0
- agents/realtime/model_events.py +12 -0
- agents/realtime/model_inputs.py +18 -1
- agents/realtime/openai_realtime.py +696 -150
- agents/realtime/session.py +243 -23
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +949 -168
- agents/run_context.py +13 -2
- agents/stream_events.py +1 -0
- agents/strict_schema.py +14 -0
- agents/tool.py +413 -15
- agents/tool_context.py +22 -1
- agents/tool_guardrails.py +279 -0
- agents/tracing/__init__.py +2 -0
- agents/tracing/config.py +9 -0
- agents/tracing/create.py +4 -0
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/processors.py +65 -54
- agents/tracing/provider.py +64 -7
- agents/tracing/spans.py +105 -0
- agents/tracing/traces.py +116 -16
- agents/usage.py +134 -12
- agents/util/_json.py +19 -1
- agents/util/_transforms.py +12 -2
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +17 -9
- agents/voice/pipeline.py +2 -0
- agents/voice/pipeline_config.py +4 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
- openai_agents-0.6.8.dist-info/RECORD +134 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.8.dist-info/RECORD +0 -103
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
agents/run_context.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Any, Generic
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Generic
|
|
3
3
|
|
|
4
4
|
from typing_extensions import TypeVar
|
|
5
5
|
|
|
6
6
|
from .usage import Usage
|
|
7
7
|
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .items import TResponseInputItem
|
|
10
|
+
|
|
8
11
|
TContext = TypeVar("TContext", default=Any)
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
@dataclass
|
|
14
|
+
@dataclass(eq=False)
|
|
12
15
|
class RunContextWrapper(Generic[TContext]):
|
|
13
16
|
"""This wraps the context object that you passed to `Runner.run()`. It also contains
|
|
14
17
|
information about the usage of the agent run so far.
|
|
@@ -24,3 +27,11 @@ class RunContextWrapper(Generic[TContext]):
|
|
|
24
27
|
"""The usage of the agent run so far. For streamed responses, the usage will be stale until the
|
|
25
28
|
last chunk of the stream is processed.
|
|
26
29
|
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(eq=False)
|
|
33
|
+
class AgentHookContext(RunContextWrapper[TContext]):
|
|
34
|
+
"""Context passed to agent hooks (on_start, on_end)."""
|
|
35
|
+
|
|
36
|
+
turn_input: "list[TResponseInputItem]" = field(default_factory=list)
|
|
37
|
+
"""The input items for the current turn."""
|
agents/stream_events.py
CHANGED
agents/strict_schema.py
CHANGED
|
@@ -87,6 +87,20 @@ def _ensure_strict_json_schema(
|
|
|
87
87
|
for i, variant in enumerate(any_of)
|
|
88
88
|
]
|
|
89
89
|
|
|
90
|
+
# oneOf is not supported by OpenAI's structured outputs in nested contexts,
|
|
91
|
+
# so we convert it to anyOf which provides equivalent functionality for
|
|
92
|
+
# discriminated unions
|
|
93
|
+
one_of = json_schema.get("oneOf")
|
|
94
|
+
if is_list(one_of):
|
|
95
|
+
existing_any_of = json_schema.get("anyOf", [])
|
|
96
|
+
if not is_list(existing_any_of):
|
|
97
|
+
existing_any_of = []
|
|
98
|
+
json_schema["anyOf"] = existing_any_of + [
|
|
99
|
+
_ensure_strict_json_schema(variant, path=(*path, "oneOf", str(i)), root=root)
|
|
100
|
+
for i, variant in enumerate(one_of)
|
|
101
|
+
]
|
|
102
|
+
json_schema.pop("oneOf")
|
|
103
|
+
|
|
90
104
|
# intersections
|
|
91
105
|
all_of = json_schema.get("allOf")
|
|
92
106
|
if is_list(all_of):
|
agents/tool.py
CHANGED
|
@@ -2,9 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import json
|
|
5
|
+
import weakref
|
|
5
6
|
from collections.abc import Awaitable
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from typing import
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
10
|
+
Any,
|
|
11
|
+
Callable,
|
|
12
|
+
Generic,
|
|
13
|
+
Literal,
|
|
14
|
+
Protocol,
|
|
15
|
+
TypeVar,
|
|
16
|
+
Union,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
8
20
|
|
|
9
21
|
from openai.types.responses.file_search_tool_param import Filters, RankingOptions
|
|
10
22
|
from openai.types.responses.response_computer_tool_call import (
|
|
@@ -13,25 +25,29 @@ from openai.types.responses.response_computer_tool_call import (
|
|
|
13
25
|
)
|
|
14
26
|
from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
|
|
15
27
|
from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
|
|
28
|
+
from openai.types.responses.web_search_tool import Filters as WebSearchToolFilters
|
|
16
29
|
from openai.types.responses.web_search_tool_param import UserLocation
|
|
17
|
-
from pydantic import ValidationError
|
|
30
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError, model_validator
|
|
18
31
|
from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict
|
|
19
32
|
|
|
20
33
|
from . import _debug
|
|
21
34
|
from .computer import AsyncComputer, Computer
|
|
22
|
-
from .
|
|
35
|
+
from .editor import ApplyPatchEditor
|
|
36
|
+
from .exceptions import ModelBehaviorError, UserError
|
|
23
37
|
from .function_schema import DocstringStyle, function_schema
|
|
24
|
-
from .items import RunItem
|
|
25
38
|
from .logger import logger
|
|
26
39
|
from .run_context import RunContextWrapper
|
|
27
40
|
from .strict_schema import ensure_strict_json_schema
|
|
28
41
|
from .tool_context import ToolContext
|
|
42
|
+
from .tool_guardrails import ToolInputGuardrail, ToolOutputGuardrail
|
|
29
43
|
from .tracing import SpanError
|
|
30
44
|
from .util import _error_tracing
|
|
31
45
|
from .util._types import MaybeAwaitable
|
|
32
46
|
|
|
33
47
|
if TYPE_CHECKING:
|
|
34
48
|
from .agent import Agent, AgentBase
|
|
49
|
+
from .items import RunItem
|
|
50
|
+
|
|
35
51
|
|
|
36
52
|
ToolParams = ParamSpec("ToolParams")
|
|
37
53
|
|
|
@@ -46,6 +62,123 @@ ToolFunction = Union[
|
|
|
46
62
|
]
|
|
47
63
|
|
|
48
64
|
|
|
65
|
+
class ToolOutputText(BaseModel):
|
|
66
|
+
"""Represents a tool output that should be sent to the model as text."""
|
|
67
|
+
|
|
68
|
+
type: Literal["text"] = "text"
|
|
69
|
+
text: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ToolOutputTextDict(TypedDict, total=False):
|
|
73
|
+
"""TypedDict variant for text tool outputs."""
|
|
74
|
+
|
|
75
|
+
type: Literal["text"]
|
|
76
|
+
text: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ToolOutputImage(BaseModel):
|
|
80
|
+
"""Represents a tool output that should be sent to the model as an image.
|
|
81
|
+
|
|
82
|
+
You can provide either an `image_url` (URL or data URL) or a `file_id` for previously uploaded
|
|
83
|
+
content. The optional `detail` can control vision detail.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
type: Literal["image"] = "image"
|
|
87
|
+
image_url: str | None = None
|
|
88
|
+
file_id: str | None = None
|
|
89
|
+
detail: Literal["low", "high", "auto"] | None = None
|
|
90
|
+
|
|
91
|
+
@model_validator(mode="after")
|
|
92
|
+
def check_at_least_one_required_field(self) -> ToolOutputImage:
|
|
93
|
+
"""Validate that at least one of image_url or file_id is provided."""
|
|
94
|
+
if self.image_url is None and self.file_id is None:
|
|
95
|
+
raise ValueError("At least one of image_url or file_id must be provided")
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ToolOutputImageDict(TypedDict, total=False):
|
|
100
|
+
"""TypedDict variant for image tool outputs."""
|
|
101
|
+
|
|
102
|
+
type: Literal["image"]
|
|
103
|
+
image_url: NotRequired[str]
|
|
104
|
+
file_id: NotRequired[str]
|
|
105
|
+
detail: NotRequired[Literal["low", "high", "auto"]]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ToolOutputFileContent(BaseModel):
|
|
109
|
+
"""Represents a tool output that should be sent to the model as a file.
|
|
110
|
+
|
|
111
|
+
Provide one of `file_data` (base64), `file_url`, or `file_id`. You may also
|
|
112
|
+
provide an optional `filename` when using `file_data` to hint file name.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
type: Literal["file"] = "file"
|
|
116
|
+
file_data: str | None = None
|
|
117
|
+
file_url: str | None = None
|
|
118
|
+
file_id: str | None = None
|
|
119
|
+
filename: str | None = None
|
|
120
|
+
|
|
121
|
+
@model_validator(mode="after")
|
|
122
|
+
def check_at_least_one_required_field(self) -> ToolOutputFileContent:
|
|
123
|
+
"""Validate that at least one of file_data, file_url, or file_id is provided."""
|
|
124
|
+
if self.file_data is None and self.file_url is None and self.file_id is None:
|
|
125
|
+
raise ValueError("At least one of file_data, file_url, or file_id must be provided")
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ToolOutputFileContentDict(TypedDict, total=False):
|
|
130
|
+
"""TypedDict variant for file content tool outputs."""
|
|
131
|
+
|
|
132
|
+
type: Literal["file"]
|
|
133
|
+
file_data: NotRequired[str]
|
|
134
|
+
file_url: NotRequired[str]
|
|
135
|
+
file_id: NotRequired[str]
|
|
136
|
+
filename: NotRequired[str]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
ValidToolOutputPydanticModels = Union[ToolOutputText, ToolOutputImage, ToolOutputFileContent]
|
|
140
|
+
ValidToolOutputPydanticModelsTypeAdapter: TypeAdapter[ValidToolOutputPydanticModels] = TypeAdapter(
|
|
141
|
+
ValidToolOutputPydanticModels
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
ComputerLike = Union[Computer, AsyncComputer]
|
|
145
|
+
ComputerT = TypeVar("ComputerT", bound=ComputerLike)
|
|
146
|
+
ComputerT_co = TypeVar("ComputerT_co", bound=ComputerLike, covariant=True)
|
|
147
|
+
ComputerT_contra = TypeVar("ComputerT_contra", bound=ComputerLike, contravariant=True)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ComputerCreate(Protocol[ComputerT_co]):
|
|
151
|
+
"""Initializes a computer for the current run context."""
|
|
152
|
+
|
|
153
|
+
def __call__(self, *, run_context: RunContextWrapper[Any]) -> MaybeAwaitable[ComputerT_co]: ...
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ComputerDispose(Protocol[ComputerT_contra]):
|
|
157
|
+
"""Cleans up a computer initialized for a run context."""
|
|
158
|
+
|
|
159
|
+
def __call__(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
run_context: RunContextWrapper[Any],
|
|
163
|
+
computer: ComputerT_contra,
|
|
164
|
+
) -> MaybeAwaitable[None]: ...
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class ComputerProvider(Generic[ComputerT]):
|
|
169
|
+
"""Configures create/dispose hooks for per-run computer lifecycle management."""
|
|
170
|
+
|
|
171
|
+
create: ComputerCreate[ComputerT]
|
|
172
|
+
dispose: ComputerDispose[ComputerT] | None = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
ComputerConfig = Union[
|
|
176
|
+
ComputerT,
|
|
177
|
+
ComputerCreate[ComputerT],
|
|
178
|
+
ComputerProvider[ComputerT],
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
|
|
49
182
|
@dataclass
|
|
50
183
|
class FunctionToolResult:
|
|
51
184
|
tool: FunctionTool
|
|
@@ -79,7 +212,9 @@ class FunctionTool:
|
|
|
79
212
|
1. The tool run context.
|
|
80
213
|
2. The arguments from the LLM, as a JSON string.
|
|
81
214
|
|
|
82
|
-
You must return a
|
|
215
|
+
You must return a one of the structured tool output types (e.g. ToolOutputText, ToolOutputImage,
|
|
216
|
+
ToolOutputFileContent) or a string representation of the tool output, or a list of them,
|
|
217
|
+
or something we can call `str()` on.
|
|
83
218
|
In case of errors, you can either raise an Exception (which will cause the run to fail) or
|
|
84
219
|
return a string error message (which will be sent back to the LLM).
|
|
85
220
|
"""
|
|
@@ -93,6 +228,13 @@ class FunctionTool:
|
|
|
93
228
|
and returns whether the tool is enabled. You can use this to dynamically enable/disable a tool
|
|
94
229
|
based on your context/state."""
|
|
95
230
|
|
|
231
|
+
# Tool-specific guardrails
|
|
232
|
+
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None
|
|
233
|
+
"""Optional list of input guardrails to run before invoking this tool."""
|
|
234
|
+
|
|
235
|
+
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None
|
|
236
|
+
"""Optional list of output guardrails to run after invoking this tool."""
|
|
237
|
+
|
|
96
238
|
def __post_init__(self):
|
|
97
239
|
if self.strict_json_schema:
|
|
98
240
|
self.params_json_schema = ensure_strict_json_schema(self.params_json_schema)
|
|
@@ -133,31 +275,169 @@ class WebSearchTool:
|
|
|
133
275
|
user_location: UserLocation | None = None
|
|
134
276
|
"""Optional location for the search. Lets you customize results to be relevant to a location."""
|
|
135
277
|
|
|
278
|
+
filters: WebSearchToolFilters | None = None
|
|
279
|
+
"""A filter to apply based on file attributes."""
|
|
280
|
+
|
|
136
281
|
search_context_size: Literal["low", "medium", "high"] = "medium"
|
|
137
282
|
"""The amount of context to use for the search."""
|
|
138
283
|
|
|
139
284
|
@property
|
|
140
285
|
def name(self):
|
|
141
|
-
return "
|
|
286
|
+
return "web_search"
|
|
142
287
|
|
|
143
288
|
|
|
144
|
-
@dataclass
|
|
145
|
-
class ComputerTool:
|
|
289
|
+
@dataclass(eq=False)
|
|
290
|
+
class ComputerTool(Generic[ComputerT]):
|
|
146
291
|
"""A hosted tool that lets the LLM control a computer."""
|
|
147
292
|
|
|
148
|
-
computer:
|
|
149
|
-
"""The computer implementation,
|
|
150
|
-
as well as implements the computer actions like click, screenshot, etc.
|
|
151
|
-
"""
|
|
293
|
+
computer: ComputerConfig[ComputerT]
|
|
294
|
+
"""The computer implementation, or a factory that produces a computer per run."""
|
|
152
295
|
|
|
153
296
|
on_safety_check: Callable[[ComputerToolSafetyCheckData], MaybeAwaitable[bool]] | None = None
|
|
154
297
|
"""Optional callback to acknowledge computer tool safety checks."""
|
|
155
298
|
|
|
299
|
+
def __post_init__(self) -> None:
|
|
300
|
+
_store_computer_initializer(self)
|
|
301
|
+
|
|
156
302
|
@property
|
|
157
303
|
def name(self):
|
|
158
304
|
return "computer_use_preview"
|
|
159
305
|
|
|
160
306
|
|
|
307
|
+
@dataclass
|
|
308
|
+
class _ResolvedComputer:
|
|
309
|
+
computer: ComputerLike
|
|
310
|
+
dispose: ComputerDispose[ComputerLike] | None = None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_computer_cache: weakref.WeakKeyDictionary[
|
|
314
|
+
ComputerTool[Any],
|
|
315
|
+
weakref.WeakKeyDictionary[RunContextWrapper[Any], _ResolvedComputer],
|
|
316
|
+
] = weakref.WeakKeyDictionary()
|
|
317
|
+
_computer_initializer_map: weakref.WeakKeyDictionary[ComputerTool[Any], ComputerConfig[Any]] = (
|
|
318
|
+
weakref.WeakKeyDictionary()
|
|
319
|
+
)
|
|
320
|
+
_computers_by_run_context: weakref.WeakKeyDictionary[
|
|
321
|
+
RunContextWrapper[Any], dict[ComputerTool[Any], _ResolvedComputer]
|
|
322
|
+
] = weakref.WeakKeyDictionary()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _is_computer_provider(candidate: object) -> bool:
|
|
326
|
+
return isinstance(candidate, ComputerProvider) or (
|
|
327
|
+
hasattr(candidate, "create") and callable(candidate.create)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _store_computer_initializer(tool: ComputerTool[Any]) -> None:
|
|
332
|
+
config = tool.computer
|
|
333
|
+
if callable(config) or _is_computer_provider(config):
|
|
334
|
+
_computer_initializer_map[tool] = config
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _get_computer_initializer(tool: ComputerTool[Any]) -> ComputerConfig[Any] | None:
|
|
338
|
+
if tool in _computer_initializer_map:
|
|
339
|
+
return _computer_initializer_map[tool]
|
|
340
|
+
|
|
341
|
+
if callable(tool.computer) or _is_computer_provider(tool.computer):
|
|
342
|
+
return tool.computer
|
|
343
|
+
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _track_resolved_computer(
|
|
348
|
+
*,
|
|
349
|
+
tool: ComputerTool[Any],
|
|
350
|
+
run_context: RunContextWrapper[Any],
|
|
351
|
+
resolved: _ResolvedComputer,
|
|
352
|
+
) -> None:
|
|
353
|
+
resolved_by_run = _computers_by_run_context.get(run_context)
|
|
354
|
+
if resolved_by_run is None:
|
|
355
|
+
resolved_by_run = {}
|
|
356
|
+
_computers_by_run_context[run_context] = resolved_by_run
|
|
357
|
+
resolved_by_run[tool] = resolved
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def resolve_computer(
|
|
361
|
+
*, tool: ComputerTool[Any], run_context: RunContextWrapper[Any]
|
|
362
|
+
) -> ComputerLike:
|
|
363
|
+
"""Resolve a computer for a given run context, initializing it if needed."""
|
|
364
|
+
per_context = _computer_cache.get(tool)
|
|
365
|
+
if per_context is None:
|
|
366
|
+
per_context = weakref.WeakKeyDictionary()
|
|
367
|
+
_computer_cache[tool] = per_context
|
|
368
|
+
|
|
369
|
+
cached = per_context.get(run_context)
|
|
370
|
+
if cached is not None:
|
|
371
|
+
_track_resolved_computer(tool=tool, run_context=run_context, resolved=cached)
|
|
372
|
+
return cached.computer
|
|
373
|
+
|
|
374
|
+
initializer_config = _get_computer_initializer(tool)
|
|
375
|
+
lifecycle: ComputerProvider[Any] | None = (
|
|
376
|
+
cast(ComputerProvider[Any], initializer_config)
|
|
377
|
+
if _is_computer_provider(initializer_config)
|
|
378
|
+
else None
|
|
379
|
+
)
|
|
380
|
+
initializer: ComputerCreate[Any] | None = None
|
|
381
|
+
disposer: ComputerDispose[Any] | None = lifecycle.dispose if lifecycle else None
|
|
382
|
+
|
|
383
|
+
if lifecycle is not None:
|
|
384
|
+
initializer = lifecycle.create
|
|
385
|
+
elif callable(initializer_config):
|
|
386
|
+
initializer = initializer_config
|
|
387
|
+
elif _is_computer_provider(tool.computer):
|
|
388
|
+
lifecycle_provider = cast(ComputerProvider[Any], tool.computer)
|
|
389
|
+
initializer = lifecycle_provider.create
|
|
390
|
+
disposer = lifecycle_provider.dispose
|
|
391
|
+
|
|
392
|
+
if initializer:
|
|
393
|
+
computer_candidate = initializer(run_context=run_context)
|
|
394
|
+
computer = (
|
|
395
|
+
await computer_candidate
|
|
396
|
+
if inspect.isawaitable(computer_candidate)
|
|
397
|
+
else computer_candidate
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
computer = cast(ComputerLike, tool.computer)
|
|
401
|
+
|
|
402
|
+
if not isinstance(computer, (Computer, AsyncComputer)):
|
|
403
|
+
raise UserError("The computer tool did not provide a computer instance.")
|
|
404
|
+
|
|
405
|
+
resolved = _ResolvedComputer(computer=computer, dispose=disposer)
|
|
406
|
+
per_context[run_context] = resolved
|
|
407
|
+
_track_resolved_computer(tool=tool, run_context=run_context, resolved=resolved)
|
|
408
|
+
tool.computer = computer
|
|
409
|
+
return computer
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
async def dispose_resolved_computers(*, run_context: RunContextWrapper[Any]) -> None:
|
|
413
|
+
"""Dispose any computer instances created for the provided run context."""
|
|
414
|
+
resolved_by_tool = _computers_by_run_context.pop(run_context, None)
|
|
415
|
+
if not resolved_by_tool:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
disposers: list[tuple[ComputerDispose[ComputerLike], ComputerLike]] = []
|
|
419
|
+
|
|
420
|
+
for tool, _resolved in resolved_by_tool.items():
|
|
421
|
+
per_context = _computer_cache.get(tool)
|
|
422
|
+
if per_context is not None:
|
|
423
|
+
per_context.pop(run_context, None)
|
|
424
|
+
|
|
425
|
+
initializer = _get_computer_initializer(tool)
|
|
426
|
+
if initializer is not None:
|
|
427
|
+
tool.computer = initializer
|
|
428
|
+
|
|
429
|
+
if _resolved.dispose is not None:
|
|
430
|
+
disposers.append((_resolved.dispose, _resolved.computer))
|
|
431
|
+
|
|
432
|
+
for dispose, computer in disposers:
|
|
433
|
+
try:
|
|
434
|
+
result = dispose(run_context=run_context, computer=computer)
|
|
435
|
+
if inspect.isawaitable(result):
|
|
436
|
+
await result
|
|
437
|
+
except Exception as exc:
|
|
438
|
+
logger.warning("Failed to dispose computer for run context: %s", exc)
|
|
439
|
+
|
|
440
|
+
|
|
161
441
|
@dataclass
|
|
162
442
|
class ComputerToolSafetyCheckData:
|
|
163
443
|
"""Information about a computer tool safety check."""
|
|
@@ -264,7 +544,11 @@ LocalShellExecutor = Callable[[LocalShellCommandRequest], MaybeAwaitable[str]]
|
|
|
264
544
|
|
|
265
545
|
@dataclass
|
|
266
546
|
class LocalShellTool:
|
|
267
|
-
"""A tool that allows the LLM to execute commands on a shell.
|
|
547
|
+
"""A tool that allows the LLM to execute commands on a shell.
|
|
548
|
+
|
|
549
|
+
For more details, see:
|
|
550
|
+
https://platform.openai.com/docs/guides/tools-local-shell
|
|
551
|
+
"""
|
|
268
552
|
|
|
269
553
|
executor: LocalShellExecutor
|
|
270
554
|
"""A function that executes a command on a shell."""
|
|
@@ -274,12 +558,109 @@ class LocalShellTool:
|
|
|
274
558
|
return "local_shell"
|
|
275
559
|
|
|
276
560
|
|
|
561
|
+
@dataclass
|
|
562
|
+
class ShellCallOutcome:
|
|
563
|
+
"""Describes the terminal condition of a shell command."""
|
|
564
|
+
|
|
565
|
+
type: Literal["exit", "timeout"]
|
|
566
|
+
exit_code: int | None = None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _default_shell_outcome() -> ShellCallOutcome:
|
|
570
|
+
return ShellCallOutcome(type="exit")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@dataclass
|
|
574
|
+
class ShellCommandOutput:
|
|
575
|
+
"""Structured output for a single shell command execution."""
|
|
576
|
+
|
|
577
|
+
stdout: str = ""
|
|
578
|
+
stderr: str = ""
|
|
579
|
+
outcome: ShellCallOutcome = field(default_factory=_default_shell_outcome)
|
|
580
|
+
command: str | None = None
|
|
581
|
+
provider_data: dict[str, Any] | None = None
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def exit_code(self) -> int | None:
|
|
585
|
+
return self.outcome.exit_code
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def status(self) -> Literal["completed", "timeout"]:
|
|
589
|
+
return "timeout" if self.outcome.type == "timeout" else "completed"
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@dataclass
|
|
593
|
+
class ShellResult:
|
|
594
|
+
"""Result returned by a shell executor."""
|
|
595
|
+
|
|
596
|
+
output: list[ShellCommandOutput]
|
|
597
|
+
max_output_length: int | None = None
|
|
598
|
+
provider_data: dict[str, Any] | None = None
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@dataclass
|
|
602
|
+
class ShellActionRequest:
|
|
603
|
+
"""Action payload for a next-generation shell call."""
|
|
604
|
+
|
|
605
|
+
commands: list[str]
|
|
606
|
+
timeout_ms: int | None = None
|
|
607
|
+
max_output_length: int | None = None
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@dataclass
|
|
611
|
+
class ShellCallData:
|
|
612
|
+
"""Normalized shell call data provided to shell executors."""
|
|
613
|
+
|
|
614
|
+
call_id: str
|
|
615
|
+
action: ShellActionRequest
|
|
616
|
+
status: Literal["in_progress", "completed"] | None = None
|
|
617
|
+
raw: Any | None = None
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@dataclass
|
|
621
|
+
class ShellCommandRequest:
|
|
622
|
+
"""A request to execute a modern shell call."""
|
|
623
|
+
|
|
624
|
+
ctx_wrapper: RunContextWrapper[Any]
|
|
625
|
+
data: ShellCallData
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
ShellExecutor = Callable[[ShellCommandRequest], MaybeAwaitable[Union[str, ShellResult]]]
|
|
629
|
+
"""Executes a shell command sequence and returns either text or structured output."""
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
@dataclass
|
|
633
|
+
class ShellTool:
|
|
634
|
+
"""Next-generation shell tool. LocalShellTool will be deprecated in favor of this."""
|
|
635
|
+
|
|
636
|
+
executor: ShellExecutor
|
|
637
|
+
name: str = "shell"
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def type(self) -> str:
|
|
641
|
+
return "shell"
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@dataclass
|
|
645
|
+
class ApplyPatchTool:
|
|
646
|
+
"""Hosted apply_patch tool. Lets the model request file mutations via unified diffs."""
|
|
647
|
+
|
|
648
|
+
editor: ApplyPatchEditor
|
|
649
|
+
name: str = "apply_patch"
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def type(self) -> str:
|
|
653
|
+
return "apply_patch"
|
|
654
|
+
|
|
655
|
+
|
|
277
656
|
Tool = Union[
|
|
278
657
|
FunctionTool,
|
|
279
658
|
FileSearchTool,
|
|
280
659
|
WebSearchTool,
|
|
281
|
-
ComputerTool,
|
|
660
|
+
ComputerTool[Any],
|
|
282
661
|
HostedMCPTool,
|
|
662
|
+
ShellTool,
|
|
663
|
+
ApplyPatchTool,
|
|
283
664
|
LocalShellTool,
|
|
284
665
|
ImageGenerationTool,
|
|
285
666
|
CodeInterpreterTool,
|
|
@@ -306,6 +687,8 @@ def function_tool(
|
|
|
306
687
|
failure_error_function: ToolErrorFunction | None = None,
|
|
307
688
|
strict_mode: bool = True,
|
|
308
689
|
is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
|
|
690
|
+
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
|
|
691
|
+
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
|
|
309
692
|
) -> FunctionTool:
|
|
310
693
|
"""Overload for usage as @function_tool (no parentheses)."""
|
|
311
694
|
...
|
|
@@ -321,6 +704,8 @@ def function_tool(
|
|
|
321
704
|
failure_error_function: ToolErrorFunction | None = None,
|
|
322
705
|
strict_mode: bool = True,
|
|
323
706
|
is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
|
|
707
|
+
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
|
|
708
|
+
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
|
|
324
709
|
) -> Callable[[ToolFunction[...]], FunctionTool]:
|
|
325
710
|
"""Overload for usage as @function_tool(...)."""
|
|
326
711
|
...
|
|
@@ -336,6 +721,8 @@ def function_tool(
|
|
|
336
721
|
failure_error_function: ToolErrorFunction | None = default_tool_error_function,
|
|
337
722
|
strict_mode: bool = True,
|
|
338
723
|
is_enabled: bool | Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]] = True,
|
|
724
|
+
tool_input_guardrails: list[ToolInputGuardrail[Any]] | None = None,
|
|
725
|
+
tool_output_guardrails: list[ToolOutputGuardrail[Any]] | None = None,
|
|
339
726
|
) -> FunctionTool | Callable[[ToolFunction[...]], FunctionTool]:
|
|
340
727
|
"""
|
|
341
728
|
Decorator to create a FunctionTool from a function. By default, we will:
|
|
@@ -367,6 +754,8 @@ def function_tool(
|
|
|
367
754
|
is_enabled: Whether the tool is enabled. Can be a bool or a callable that takes the run
|
|
368
755
|
context and agent and returns whether the tool is enabled. Disabled tools are hidden
|
|
369
756
|
from the LLM at runtime.
|
|
757
|
+
tool_input_guardrails: Optional list of guardrails to run before invoking the tool.
|
|
758
|
+
tool_output_guardrails: Optional list of guardrails to run after the tool returns.
|
|
370
759
|
"""
|
|
371
760
|
|
|
372
761
|
def _create_function_tool(the_func: ToolFunction[...]) -> FunctionTool:
|
|
@@ -448,6 +837,13 @@ def function_tool(
|
|
|
448
837
|
},
|
|
449
838
|
)
|
|
450
839
|
)
|
|
840
|
+
if _debug.DONT_LOG_TOOL_DATA:
|
|
841
|
+
logger.debug(f"Tool {schema.name} failed")
|
|
842
|
+
else:
|
|
843
|
+
logger.error(
|
|
844
|
+
f"Tool {schema.name} failed: {input} {e}",
|
|
845
|
+
exc_info=e,
|
|
846
|
+
)
|
|
451
847
|
return result
|
|
452
848
|
|
|
453
849
|
return FunctionTool(
|
|
@@ -457,6 +853,8 @@ def function_tool(
|
|
|
457
853
|
on_invoke_tool=_on_invoke_tool,
|
|
458
854
|
strict_json_schema=strict_mode,
|
|
459
855
|
is_enabled=is_enabled,
|
|
856
|
+
tool_input_guardrails=tool_input_guardrails,
|
|
857
|
+
tool_output_guardrails=tool_output_guardrails,
|
|
460
858
|
)
|
|
461
859
|
|
|
462
860
|
# If func is actually a callable, we were used as @function_tool with no parentheses
|
agents/tool_context.py
CHANGED
|
@@ -14,6 +14,10 @@ def _assert_must_pass_tool_name() -> str:
|
|
|
14
14
|
raise ValueError("tool_name must be passed to ToolContext")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _assert_must_pass_tool_arguments() -> str:
|
|
18
|
+
raise ValueError("tool_arguments must be passed to ToolContext")
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
@dataclass
|
|
18
22
|
class ToolContext(RunContextWrapper[TContext]):
|
|
19
23
|
"""The context of a tool call."""
|
|
@@ -24,6 +28,12 @@ class ToolContext(RunContextWrapper[TContext]):
|
|
|
24
28
|
tool_call_id: str = field(default_factory=_assert_must_pass_tool_call_id)
|
|
25
29
|
"""The ID of the tool call."""
|
|
26
30
|
|
|
31
|
+
tool_arguments: str = field(default_factory=_assert_must_pass_tool_arguments)
|
|
32
|
+
"""The raw arguments string of the tool call."""
|
|
33
|
+
|
|
34
|
+
tool_call: Optional[ResponseFunctionToolCall] = None
|
|
35
|
+
"""The tool call object associated with this invocation."""
|
|
36
|
+
|
|
27
37
|
@classmethod
|
|
28
38
|
def from_agent_context(
|
|
29
39
|
cls,
|
|
@@ -39,4 +49,15 @@ class ToolContext(RunContextWrapper[TContext]):
|
|
|
39
49
|
f.name: getattr(context, f.name) for f in fields(RunContextWrapper) if f.init
|
|
40
50
|
}
|
|
41
51
|
tool_name = tool_call.name if tool_call is not None else _assert_must_pass_tool_name()
|
|
42
|
-
|
|
52
|
+
tool_args = (
|
|
53
|
+
tool_call.arguments if tool_call is not None else _assert_must_pass_tool_arguments()
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
tool_context = cls(
|
|
57
|
+
tool_name=tool_name,
|
|
58
|
+
tool_call_id=tool_call_id,
|
|
59
|
+
tool_arguments=tool_args,
|
|
60
|
+
tool_call=tool_call,
|
|
61
|
+
**base_values,
|
|
62
|
+
)
|
|
63
|
+
return tool_context
|