@@ -97,6 +97,18 @@ def content_bytes(self):
9797 def base64_content (self ):
9898 return base64 .b64encode (self .content_bytes ()).decode ("utf-8" )
9999
100+ def __repr__ (self ):
101+ info = [f"<Attachment: { self .id ()} " ]
102+ if self .type :
103+ info .append (f'type="{ self .type } "' )
104+ if self .path :
105+ info .append (f'path="{ self .path } "' )
106+ if self .url :
107+ info .append (f'url="{ self .url } "' )
108+ if self .content :
109+ info .append (f"content={ len (self .content )} bytes" )
110+ return " " .join (info ) + ">"
111+
100112 @classmethod
101113 def from_row (cls , row ):
102114 return cls (
@@ -261,10 +273,19 @@ class ToolCall:
261273class ToolResult :
262274 name : str
263275 output : str
276+ attachments : List [Attachment ] = field (default_factory = list )
264277 tool_call_id : Optional [str ] = None
265278 instance : Optional [Toolbox ] = None
266279
267280
281+ @dataclass
282+ class ToolOutput :
283+ "Tool functions can return output with extra attachments"
284+
285+ output : Optional [Union [str , dict , list , bool , int , float ]] = None
286+ attachments : List [Attachment ] = field (default_factory = list )
287+
288+
268289class CancelToolCall (Exception ):
269290 pass
270291
@@ -887,16 +908,40 @@ def log_to_db(self, db):
887908 instance_id = tool_result .instance .instance_id
888909 except AttributeError :
889910 pass
890- db ["tool_results" ].insert (
891- {
892- "response_id" : response_id ,
893- "tool_id" : tool_ids_by_name .get (tool_result .name ) or None ,
894- "name" : tool_result .name ,
895- "output" : tool_result .output ,
896- "tool_call_id" : tool_result .tool_call_id ,
897- "instance_id" : instance_id ,
898- }
911+ tool_result_id = (
912+ db ["tool_results" ]
913+ .insert (
914+ {
915+ "response_id" : response_id ,
916+ "tool_id" : tool_ids_by_name .get (tool_result .name ) or None ,
917+ "name" : tool_result .name ,
918+ "output" : tool_result .output ,
919+ "tool_call_id" : tool_result .tool_call_id ,
920+ "instance_id" : instance_id ,
921+ }
922+ )
923+ .last_pk
899924 )
925+ # Persist attachments for tool results
926+ for index , attachment in enumerate (tool_result .attachments ):
927+ attachment_id = attachment .id ()
928+ db ["attachments" ].insert (
929+ {
930+ "id" : attachment_id ,
931+ "type" : attachment .resolve_type (),
932+ "path" : attachment .path ,
933+ "url" : attachment .url ,
934+ "content" : attachment .content ,
935+ },
936+ replace = True ,
937+ )
938+ db ["tool_results_attachments" ].insert (
939+ {
940+ "tool_result_id" : tool_result_id ,
941+ "attachment_id" : attachment_id ,
942+ "order" : index ,
943+ },
944+ )
900945
901946
902947class Response (_BaseResponse ):
@@ -964,12 +1009,18 @@ def execute_tool_calls(
9641009 "No implementation available for tool: {}" .format (tool_call .name )
9651010 )
9661011
1012+ attachments = []
1013+
9671014 try :
9681015 if asyncio .iscoroutinefunction (tool .implementation ):
9691016 result = asyncio .run (tool .implementation (** tool_call .arguments ))
9701017 else :
9711018 result = tool .implementation (** tool_call .arguments )
9721019
1020+ if isinstance (result , ToolOutput ):
1021+ attachments = result .attachments
1022+ result = result .output
1023+
9731024 if not isinstance (result , str ):
9741025 result = json .dumps (result , default = repr )
9751026 except Exception as ex :
@@ -978,6 +1029,7 @@ def execute_tool_calls(
9781029 tool_result_obj = ToolResult (
9791030 name = tool_call .name ,
9801031 output = result ,
1032+ attachments = attachments ,
9811033 tool_call_id = tool_call .tool_call_id ,
9821034 instance = _get_instance (tool .implementation ),
9831035 )
@@ -1125,8 +1177,12 @@ async def run_async(tc=tc, tool=tool, idx=idx):
11251177 if inspect .isawaitable (cb ):
11261178 await cb
11271179
1180+ attachments = []
11281181 try :
11291182 result = await tool .implementation (** tc .arguments )
1183+ if isinstance (result , ToolOutput ):
1184+ attachments .extend (result .attachments )
1185+ result = result .output
11301186 output = (
11311187 result
11321188 if isinstance (result , str )
@@ -1138,6 +1194,7 @@ async def run_async(tc=tc, tool=tool, idx=idx):
11381194 tr = ToolResult (
11391195 name = tc .name ,
11401196 output = output ,
1197+ attachments = attachments ,
11411198 tool_call_id = tc .tool_call_id ,
11421199 instance = _get_instance (tool .implementation ),
11431200 )
@@ -1159,10 +1216,14 @@ async def run_async(tc=tc, tool=tool, idx=idx):
11591216 if inspect .isawaitable (cb ):
11601217 await cb
11611218
1219+ attachments = []
11621220 try :
11631221 res = tool .implementation (** tc .arguments )
11641222 if inspect .isawaitable (res ):
11651223 res = await res
1224+ if isinstance (res , ToolOutput ):
1225+ attachments .extend (res .attachments )
1226+ res = res .output
11661227 output = (
11671228 res if isinstance (res , str ) else json .dumps (res , default = repr )
11681229 )
@@ -1172,6 +1233,7 @@ async def run_async(tc=tc, tool=tool, idx=idx):
11721233 tr = ToolResult (
11731234 name = tc .name ,
11741235 output = output ,
1236+ attachments = attachments ,
11751237 tool_call_id = tc .tool_call_id ,
11761238 instance = _get_instance (tool .implementation ),
11771239 )
@@ -1427,6 +1489,9 @@ def responses(self) -> Iterator[Response]:
14271489 tool_results = current_response .execute_tool_calls (
14281490 before_call = self .before_call , after_call = self .after_call
14291491 )
1492+ attachments = []
1493+ for tool_result in tool_results :
1494+ attachments .extend (tool_result .attachments )
14301495 if tool_results :
14311496 current_response = Response (
14321497 Prompt (
@@ -1435,6 +1500,7 @@ def responses(self) -> Iterator[Response]:
14351500 tools = current_response .prompt .tools ,
14361501 tool_results = tool_results ,
14371502 options = self .prompt .options ,
1503+ attachments = attachments ,
14381504 ),
14391505 self .model ,
14401506 stream = self .stream ,
@@ -1479,12 +1545,16 @@ async def responses(self) -> AsyncIterator[AsyncResponse]:
14791545 before_call = self .before_call , after_call = self .after_call
14801546 )
14811547 if tool_results :
1548+ attachments = []
1549+ for tool_result in tool_results :
1550+ attachments .extend (tool_result .attachments )
14821551 prompt = Prompt (
14831552 "" ,
14841553 self .model ,
14851554 tools = current_response .prompt .tools ,
14861555 tool_results = tool_results ,
14871556 options = self .prompt .options ,
1557+ attachments = attachments ,
14881558 )
14891559 current_response = AsyncResponse (
14901560 prompt ,
0 commit comments