-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Expand file tree
/
Copy pathitems.py
More file actions
404 lines (326 loc) · 13.9 KB
/
items.py
File metadata and controls
404 lines (326 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
from __future__ import annotations
import abc
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, Union
import pydantic
from openai.types.responses import (
Response,
ResponseComputerToolCall,
ResponseFileSearchToolCall,
ResponseFunctionToolCall,
ResponseFunctionWebSearch,
ResponseInputItemParam,
ResponseOutputItem,
ResponseOutputMessage,
ResponseOutputRefusal,
ResponseOutputText,
ResponseStreamEvent,
)
from openai.types.responses.response_code_interpreter_tool_call import (
ResponseCodeInterpreterToolCall,
)
from openai.types.responses.response_function_call_output_item_list_param import (
ResponseFunctionCallOutputItemListParam,
ResponseFunctionCallOutputItemParam,
)
from openai.types.responses.response_input_file_content_param import ResponseInputFileContentParam
from openai.types.responses.response_input_image_content_param import ResponseInputImageContentParam
from openai.types.responses.response_input_item_param import (
ComputerCallOutput,
FunctionCallOutput,
LocalShellCallOutput,
McpApprovalResponse,
)
from openai.types.responses.response_output_item import (
ImageGenerationCall,
LocalShellCall,
McpApprovalRequest,
McpCall,
McpListTools,
)
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
from pydantic import BaseModel
from typing_extensions import TypeAlias, assert_never
from .exceptions import AgentsException, ModelBehaviorError
from .logger import logger
from .tool import (
ToolOutputFileContent,
ToolOutputImage,
ToolOutputText,
ValidToolOutputPydanticModels,
ValidToolOutputPydanticModelsTypeAdapter,
)
from .usage import Usage
if TYPE_CHECKING:
from .agent import Agent
TResponse = Response
"""A type alias for the Response type from the OpenAI SDK."""
TResponseInputItem = ResponseInputItemParam
"""A type alias for the ResponseInputItemParam type from the OpenAI SDK."""
TResponseOutputItem = ResponseOutputItem
"""A type alias for the ResponseOutputItem type from the OpenAI SDK."""
TResponseStreamEvent = ResponseStreamEvent
"""A type alias for the ResponseStreamEvent type from the OpenAI SDK."""
T = TypeVar("T", bound=Union[TResponseOutputItem, TResponseInputItem])
@dataclass
class RunItemBase(Generic[T], abc.ABC):
agent: Agent[Any]
"""The agent whose run caused this item to be generated."""
raw_item: T
"""The raw Responses item from the run. This will always be either an output item (i.e.
`openai.types.responses.ResponseOutputItem` or an input item
(i.e. `openai.types.responses.ResponseInputItemParam`).
"""
def to_input_item(self) -> TResponseInputItem:
"""Converts this item into an input item suitable for passing to the model."""
if isinstance(self.raw_item, dict):
# We know that input items are dicts, so we can ignore the type error
return self.raw_item # type: ignore
elif isinstance(self.raw_item, BaseModel):
# All output items are Pydantic models that can be converted to input items.
return self.raw_item.model_dump(exclude_unset=True) # type: ignore
else:
raise AgentsException(f"Unexpected raw item type: {type(self.raw_item)}")
@dataclass
class MessageOutputItem(RunItemBase[ResponseOutputMessage]):
"""Represents a message from the LLM."""
raw_item: ResponseOutputMessage
"""The raw response output message."""
type: Literal["message_output_item"] = "message_output_item"
@dataclass
class HandoffCallItem(RunItemBase[ResponseFunctionToolCall]):
"""Represents a tool call for a handoff from one agent to another."""
raw_item: ResponseFunctionToolCall
"""The raw response function tool call that represents the handoff."""
type: Literal["handoff_call_item"] = "handoff_call_item"
@dataclass
class HandoffOutputItem(RunItemBase[TResponseInputItem]):
"""Represents the output of a handoff."""
raw_item: TResponseInputItem
"""The raw input item that represents the handoff taking place."""
source_agent: Agent[Any]
"""The agent that made the handoff."""
target_agent: Agent[Any]
"""The agent that is being handed off to."""
type: Literal["handoff_output_item"] = "handoff_output_item"
ToolCallItemTypes: TypeAlias = Union[
ResponseFunctionToolCall,
ResponseComputerToolCall,
ResponseFileSearchToolCall,
ResponseFunctionWebSearch,
McpCall,
ResponseCodeInterpreterToolCall,
ImageGenerationCall,
LocalShellCall,
]
"""A type that represents a tool call item."""
@dataclass
class ToolCallItem(RunItemBase[ToolCallItemTypes]):
"""Represents a tool call e.g. a function call or computer action call."""
raw_item: ToolCallItemTypes
"""The raw tool call item."""
type: Literal["tool_call_item"] = "tool_call_item"
@dataclass
class ToolCallOutputItem(
RunItemBase[Union[FunctionCallOutput, ComputerCallOutput, LocalShellCallOutput]]
):
"""Represents the output of a tool call."""
raw_item: FunctionCallOutput | ComputerCallOutput | LocalShellCallOutput
"""The raw item from the model."""
output: Any
"""The output of the tool call. This is whatever the tool call returned; the `raw_item`
contains a string representation of the output.
"""
type: Literal["tool_call_output_item"] = "tool_call_output_item"
@dataclass
class ReasoningItem(RunItemBase[ResponseReasoningItem]):
"""Represents a reasoning item."""
raw_item: ResponseReasoningItem
"""The raw reasoning item."""
type: Literal["reasoning_item"] = "reasoning_item"
@dataclass
class MCPListToolsItem(RunItemBase[McpListTools]):
"""Represents a call to an MCP server to list tools."""
raw_item: McpListTools
"""The raw MCP list tools call."""
type: Literal["mcp_list_tools_item"] = "mcp_list_tools_item"
@dataclass
class MCPApprovalRequestItem(RunItemBase[McpApprovalRequest]):
"""Represents a request for MCP approval."""
raw_item: McpApprovalRequest
"""The raw MCP approval request."""
type: Literal["mcp_approval_request_item"] = "mcp_approval_request_item"
@dataclass
class MCPApprovalResponseItem(RunItemBase[McpApprovalResponse]):
"""Represents a response to an MCP approval request."""
raw_item: McpApprovalResponse
"""The raw MCP approval response."""
type: Literal["mcp_approval_response_item"] = "mcp_approval_response_item"
RunItem: TypeAlias = Union[
MessageOutputItem,
HandoffCallItem,
HandoffOutputItem,
ToolCallItem,
ToolCallOutputItem,
ReasoningItem,
MCPListToolsItem,
MCPApprovalRequestItem,
MCPApprovalResponseItem,
]
"""An item generated by an agent."""
@pydantic.dataclasses.dataclass
class ModelResponse:
output: list[TResponseOutputItem]
"""A list of outputs (messages, tool calls, etc) generated by the model"""
usage: Usage
"""The usage information for the response."""
response_id: str | None
"""An ID for the response which can be used to refer to the response in subsequent calls to the
model. Not supported by all model providers.
If using OpenAI models via the Responses API, this is the `response_id` parameter, and it can
be passed to `Runner.run`.
"""
def to_input_items(self) -> list[TResponseInputItem]:
"""Convert the output into a list of input items suitable for passing to the model."""
# We happen to know that the shape of the Pydantic output items are the same as the
# equivalent TypedDict input items, so we can just convert each one.
# This is also tested via unit tests.
return [it.model_dump(exclude_unset=True) for it in self.output] # type: ignore
class ItemHelpers:
@classmethod
def extract_last_content(cls, message: TResponseOutputItem) -> str:
"""Extracts the last text content or refusal from a message."""
if not isinstance(message, ResponseOutputMessage):
return ""
if not message.content:
return ""
last_content = message.content[-1]
if isinstance(last_content, ResponseOutputText):
return last_content.text
elif isinstance(last_content, ResponseOutputRefusal):
return last_content.refusal
else:
raise ModelBehaviorError(f"Unexpected content type: {type(last_content)}")
@classmethod
def extract_last_text(cls, message: TResponseOutputItem) -> str | None:
"""Extracts the last text content from a message, if any. Ignores refusals."""
if isinstance(message, ResponseOutputMessage):
if not message.content:
return None
last_content = message.content[-1]
if isinstance(last_content, ResponseOutputText):
return last_content.text
return None
@classmethod
def input_to_new_input_list(
cls, input: str | list[TResponseInputItem]
) -> list[TResponseInputItem]:
"""Converts a string or list of input items into a list of input items."""
if isinstance(input, str):
return [
{
"content": input,
"role": "user",
}
]
return input.copy()
@classmethod
def text_message_outputs(cls, items: list[RunItem]) -> str:
"""Concatenates all the text content from a list of message output items."""
text = ""
for item in items:
if isinstance(item, MessageOutputItem):
text += cls.text_message_output(item)
return text
@classmethod
def text_message_output(cls, message: MessageOutputItem) -> str:
"""Extracts all the text content from a single message output item."""
text = ""
for item in message.raw_item.content:
if isinstance(item, ResponseOutputText):
text += item.text
return text
@classmethod
def tool_call_output_item(
cls, tool_call: ResponseFunctionToolCall, output: Any
) -> FunctionCallOutput:
"""Creates a tool call output item from a tool call and its output.
Accepts either plain values (stringified) or structured outputs using
input_text/input_image/input_file shapes. Structured outputs may be
provided as Pydantic models or dicts, or an iterable of such items.
"""
converted_output = cls._convert_tool_output(output)
return {
"call_id": tool_call.call_id,
"output": converted_output,
"type": "function_call_output",
}
@classmethod
def _convert_tool_output(cls, output: Any) -> str | ResponseFunctionCallOutputItemListParam:
"""Converts a tool return value into an output acceptable by the Responses API."""
# If the output is either a single or list of the known structured output types, convert to
# ResponseFunctionCallOutputItemListParam. Else, just stringify.
if isinstance(output, (list, tuple)):
maybe_converted_output_list = [
cls._maybe_get_output_as_structured_function_output(item) for item in output
]
if all(maybe_converted_output_list):
return [
cls._convert_single_tool_output_pydantic_model(item)
for item in maybe_converted_output_list
if item is not None
]
else:
return str(output)
else:
maybe_converted_output = cls._maybe_get_output_as_structured_function_output(output)
if maybe_converted_output:
return [cls._convert_single_tool_output_pydantic_model(maybe_converted_output)]
else:
return str(output)
@classmethod
def _maybe_get_output_as_structured_function_output(
cls, output: Any
) -> ValidToolOutputPydanticModels | None:
if isinstance(output, (ToolOutputText, ToolOutputImage, ToolOutputFileContent)):
return output
elif isinstance(output, dict):
try:
return ValidToolOutputPydanticModelsTypeAdapter.validate_python(output)
except pydantic.ValidationError:
logger.debug("dict was not a valid tool output pydantic model")
return None
return None
@classmethod
def _convert_single_tool_output_pydantic_model(
cls, output: ValidToolOutputPydanticModels
) -> ResponseFunctionCallOutputItemParam:
if isinstance(output, ToolOutputText):
return {"type": "input_text", "text": output.text}
elif isinstance(output, ToolOutputImage):
# Forward all provided optional fields so the Responses API receives
# the correct identifiers and settings for the image resource.
result: ResponseInputImageContentParam = {"type": "input_image"}
if output.image_url is not None:
result["image_url"] = output.image_url
if output.file_id is not None:
result["file_id"] = output.file_id
if output.detail is not None:
result["detail"] = output.detail
return result
elif isinstance(output, ToolOutputFileContent):
# Forward all provided optional fields so the Responses API receives
# the correct identifiers and metadata for the file resource.
result_file: ResponseInputFileContentParam = {"type": "input_file"}
if output.file_data is not None:
result_file["file_data"] = output.file_data
if output.file_url is not None:
result_file["file_url"] = output.file_url
if output.file_id is not None:
result_file["file_id"] = output.file_id
if output.filename is not None:
result_file["filename"] = output.filename
return result_file
else:
assert_never(output)
raise ValueError(f"Unexpected tool output type: {output}")