Skip to content

Commit a1647a6

Browse files
committed
Add support for local shell, image generator, code interpreter tools
1 parent e72b772 commit a1647a6

File tree

7 files changed

+334
-20
lines changed

7 files changed

+334
-20
lines changed

examples/tools/code_interpreter.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import asyncio
2+
3+
from agents import Agent, CodeInterpreterTool, Runner, trace
4+
5+
6+
async def main():
7+
agent = Agent(
8+
name="Code interpreter",
9+
instructions="You love doing math.",
10+
tools=[
11+
CodeInterpreterTool(
12+
tool_config={"type": "code_interpreter", "container": {"type": "auto"}},
13+
)
14+
],
15+
)
16+
17+
with trace("Code interpreter example"):
18+
print("Solving math problem...")
19+
result = Runner.run_streamed(agent, "What is the square root of273 * 312821 plus 1782?")
20+
async for event in result.stream_events():
21+
if (
22+
event.type == "run_item_stream_event"
23+
and event.item.type == "tool_call_item"
24+
and event.item.raw_item.type == "code_interpreter_call"
25+
):
26+
print(f"Code interpreter code:\n```\n{event.item.raw_item.code}\n```\n")
27+
elif event.type == "run_item_stream_event":
28+
print(f"Other event: {event.item.type}")
29+
30+
print(f"Final output: {result.final_output}")
31+
32+
33+
if __name__ == "__main__":
34+
asyncio.run(main())

examples/tools/image_generator.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import asyncio
2+
import base64
3+
import os
4+
import subprocess
5+
import sys
6+
import tempfile
7+
8+
from agents import Agent, ImageGenerationTool, Runner, trace
9+
10+
11+
def open_file(path: str) -> None:
12+
if sys.platform.startswith("darwin"):
13+
subprocess.run(["open", path], check=False) # macOS
14+
elif os.name == "nt":
15+
os.startfile(path) # type-ignore[attr-define] Windows only
16+
elif os.name == "posix":
17+
subprocess.run(["xdg-open", path], check=False) # Linux/Unix
18+
else:
19+
print(f"Don't know how to open files on this platform: {sys.platform}")
20+
21+
22+
async def main():
23+
agent = Agent(
24+
name="Image generator",
25+
instructions="You are a helpful agent.",
26+
tools=[
27+
ImageGenerationTool(
28+
tool_config={"type": "image_generation", "quality": "low"},
29+
)
30+
],
31+
)
32+
33+
with trace("Image generation example"):
34+
print("Generating image, this may take a while...")
35+
result = await Runner.run(
36+
agent, "Create an image of a frog eating a pizza, comic book style."
37+
)
38+
print(result.final_output)
39+
for item in result.new_items:
40+
if (
41+
item.type == "tool_call_item"
42+
and item.raw_item.type == "image_generation_call"
43+
and (img_result := item.raw_item.result)
44+
):
45+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
46+
tmp.write(base64.b64decode(img_result))
47+
temp_path = tmp.name
48+
49+
# Open the image
50+
open_file(temp_path)
51+
52+
53+
if __name__ == "__main__":
54+
asyncio.run(main())

src/agents/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@
5454
StreamEvent,
5555
)
5656
from .tool import (
57+
CodeInterpreterTool,
5758
ComputerTool,
5859
FileSearchTool,
5960
FunctionTool,
6061
FunctionToolResult,
6162
HostedMCPTool,
63+
ImageGenerationTool,
64+
LocalShellCommandRequest,
65+
LocalShellExecutor,
66+
LocalShellTool,
6267
MCPToolApprovalFunction,
6368
MCPToolApprovalFunctionResult,
6469
MCPToolApprovalRequest,
@@ -210,6 +215,11 @@ def enable_verbose_stdout_logging():
210215
"FunctionToolResult",
211216
"ComputerTool",
212217
"FileSearchTool",
218+
"CodeInterpreterTool",
219+
"ImageGenerationTool",
220+
"LocalShellCommandRequest",
221+
"LocalShellExecutor",
222+
"LocalShellTool",
213223
"Tool",
214224
"WebSearchTool",
215225
"HostedMCPTool",

src/agents/_run_impl.py

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
ResponseFunctionWebSearch,
1515
ResponseOutputMessage,
1616
)
17+
from openai.types.responses.response_code_interpreter_tool_call import (
18+
ResponseCodeInterpreterToolCall,
19+
)
1720
from openai.types.responses.response_computer_tool_call import (
1821
ActionClick,
1922
ActionDoubleClick,
@@ -26,7 +29,12 @@
2629
ActionWait,
2730
)
2831
from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse
29-
from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools
32+
from openai.types.responses.response_output_item import (
33+
ImageGenerationCall,
34+
LocalShellCall,
35+
McpApprovalRequest,
36+
McpListTools,
37+
)
3038
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
3139

3240
from .agent import Agent, ToolsToFinalOutputResult
@@ -61,6 +69,8 @@
6169
FunctionTool,
6270
FunctionToolResult,
6371
HostedMCPTool,
72+
LocalShellCommandRequest,
73+
LocalShellTool,
6474
MCPToolApprovalRequest,
6575
Tool,
6676
)
@@ -129,12 +139,19 @@ class ToolRunMCPApprovalRequest:
129139
mcp_tool: HostedMCPTool
130140

131141

142+
@dataclass
143+
class ToolRunLocalShellCall:
144+
tool_call: LocalShellCall
145+
local_shell_tool: LocalShellTool
146+
147+
132148
@dataclass
133149
class ProcessedResponse:
134150
new_items: list[RunItem]
135151
handoffs: list[ToolRunHandoff]
136152
functions: list[ToolRunFunction]
137153
computer_actions: list[ToolRunComputerAction]
154+
local_shell_calls: list[ToolRunLocalShellCall]
138155
tools_used: list[str] # Names of all tools used, including hosted tools
139156
mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks
140157

@@ -146,6 +163,7 @@ def has_tools_or_approvals_to_run(self) -> bool:
146163
self.handoffs,
147164
self.functions,
148165
self.computer_actions,
166+
self.local_shell_calls,
149167
self.mcp_approval_requests,
150168
]
151169
)
@@ -371,11 +389,15 @@ def process_model_response(
371389
run_handoffs = []
372390
functions = []
373391
computer_actions = []
392+
local_shell_calls = []
374393
mcp_approval_requests = []
375394
tools_used: list[str] = []
376395
handoff_map = {handoff.tool_name: handoff for handoff in handoffs}
377396
function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)}
378397
computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None)
398+
local_shell_tool = next(
399+
(tool for tool in all_tools if isinstance(tool, LocalShellTool)), None
400+
)
379401
hosted_mcp_server_map = {
380402
tool.tool_config["server_label"]: tool
381403
for tool in all_tools
@@ -434,9 +456,29 @@ def process_model_response(
434456
)
435457
elif isinstance(output, McpListTools):
436458
items.append(MCPListToolsItem(raw_item=output, agent=agent))
437-
elif isinstance(output, McpCall):
459+
elif isinstance(output, ImageGenerationCall):
460+
items.append(ToolCallItem(raw_item=output, agent=agent))
461+
tools_used.append("image_generation")
462+
elif isinstance(output, ResponseCodeInterpreterToolCall):
438463
items.append(ToolCallItem(raw_item=output, agent=agent))
439-
tools_used.append(output.name)
464+
tools_used.append("code_interpreter")
465+
elif isinstance(output, LocalShellCall):
466+
items.append(ToolCallItem(raw_item=output, agent=agent))
467+
tools_used.append("local_shell")
468+
if not local_shell_tool:
469+
_error_tracing.attach_error_to_current_span(
470+
SpanError(
471+
message="Local shell tool not found",
472+
data={},
473+
)
474+
)
475+
raise ModelBehaviorError(
476+
"Model produced local shell call without a local shell tool."
477+
)
478+
local_shell_calls.append(
479+
ToolRunLocalShellCall(tool_call=output, local_shell_tool=local_shell_tool)
480+
)
481+
440482
elif not isinstance(output, ResponseFunctionToolCall):
441483
logger.warning(f"Unexpected output type, ignoring: {type(output)}")
442484
continue
@@ -478,6 +520,7 @@ def process_model_response(
478520
handoffs=run_handoffs,
479521
functions=functions,
480522
computer_actions=computer_actions,
523+
local_shell_calls=local_shell_calls,
481524
tools_used=tools_used,
482525
mcp_approval_requests=mcp_approval_requests,
483526
)
@@ -552,6 +595,30 @@ async def run_single_tool(
552595
for tool_run, result in zip(tool_runs, results)
553596
]
554597

598+
@classmethod
599+
async def execute_local_shell_calls(
600+
cls,
601+
*,
602+
agent: Agent[TContext],
603+
calls: list[ToolRunLocalShellCall],
604+
context_wrapper: RunContextWrapper[TContext],
605+
hooks: RunHooks[TContext],
606+
config: RunConfig,
607+
) -> list[RunItem]:
608+
results: list[RunItem] = []
609+
# Need to run these serially, because each call can affect the local shell state
610+
for call in calls:
611+
results.append(
612+
await LocalShellAction.execute(
613+
agent=agent,
614+
call=call,
615+
hooks=hooks,
616+
context_wrapper=context_wrapper,
617+
config=config,
618+
)
619+
)
620+
return results
621+
555622
@classmethod
556623
async def execute_computer_actions(
557624
cls,
@@ -1021,3 +1088,54 @@ async def _get_screenshot_async(
10211088
await computer.wait()
10221089

10231090
return await computer.screenshot()
1091+
1092+
1093+
class LocalShellAction:
1094+
@classmethod
1095+
async def execute(
1096+
cls,
1097+
*,
1098+
agent: Agent[TContext],
1099+
call: ToolRunLocalShellCall,
1100+
hooks: RunHooks[TContext],
1101+
context_wrapper: RunContextWrapper[TContext],
1102+
config: RunConfig,
1103+
) -> RunItem:
1104+
await asyncio.gather(
1105+
hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool),
1106+
(
1107+
agent.hooks.on_tool_start(context_wrapper, agent, call.local_shell_tool)
1108+
if agent.hooks
1109+
else _coro.noop_coroutine()
1110+
),
1111+
)
1112+
1113+
request = LocalShellCommandRequest(
1114+
ctx_wrapper=context_wrapper,
1115+
data=call.tool_call,
1116+
)
1117+
output = call.local_shell_tool.executor(request)
1118+
if inspect.isawaitable(output):
1119+
result = await output
1120+
else:
1121+
result = output
1122+
1123+
await asyncio.gather(
1124+
hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result),
1125+
(
1126+
agent.hooks.on_tool_end(context_wrapper, agent, call.local_shell_tool, result)
1127+
if agent.hooks
1128+
else _coro.noop_coroutine()
1129+
),
1130+
)
1131+
1132+
return ToolCallOutputItem(
1133+
agent=agent,
1134+
output=output,
1135+
raw_item={
1136+
"type": "local_shell_call_output",
1137+
"id": call.tool_call.call_id,
1138+
"output": result,
1139+
# "id": "out" + call.tool_call.id, # TODO remove this, it should be optional
1140+
},
1141+
)

src/agents/items.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,22 @@
1818
ResponseOutputText,
1919
ResponseStreamEvent,
2020
)
21+
from openai.types.responses.response_code_interpreter_tool_call import (
22+
ResponseCodeInterpreterToolCall,
23+
)
2124
from openai.types.responses.response_input_item_param import (
2225
ComputerCallOutput,
2326
FunctionCallOutput,
27+
LocalShellCallOutput,
2428
McpApprovalResponse,
2529
)
26-
from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools
30+
from openai.types.responses.response_output_item import (
31+
ImageGenerationCall,
32+
LocalShellCall,
33+
McpApprovalRequest,
34+
McpCall,
35+
McpListTools,
36+
)
2737
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
2838
from pydantic import BaseModel
2939
from typing_extensions import TypeAlias
@@ -114,6 +124,9 @@ class HandoffOutputItem(RunItemBase[TResponseInputItem]):
114124
ResponseFileSearchToolCall,
115125
ResponseFunctionWebSearch,
116126
McpCall,
127+
ResponseCodeInterpreterToolCall,
128+
ImageGenerationCall,
129+
LocalShellCall,
117130
]
118131
"""A type that represents a tool call item."""
119132

@@ -129,10 +142,12 @@ class ToolCallItem(RunItemBase[ToolCallItemTypes]):
129142

130143

131144
@dataclass
132-
class ToolCallOutputItem(RunItemBase[Union[FunctionCallOutput, ComputerCallOutput]]):
145+
class ToolCallOutputItem(
146+
RunItemBase[Union[FunctionCallOutput, ComputerCallOutput, LocalShellCallOutput]]
147+
):
133148
"""Represents the output of a tool call."""
134149

135-
raw_item: FunctionCallOutput | ComputerCallOutput
150+
raw_item: FunctionCallOutput | ComputerCallOutput | LocalShellCallOutput
136151
"""The raw item from the model."""
137152

138153
output: Any

0 commit comments

Comments
 (0)