meshagent-openai 0.0.30__py3-none-any.whl → 0.0.32__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 meshagent-openai might be problematic. Click here for more details.

@@ -2,32 +2,29 @@
2
2
  from meshagent.agents.agent import Agent, AgentChatContext, AgentCallContext
3
3
  from meshagent.api import WebSocketClientProtocol, RoomClient, RoomException
4
4
  from meshagent.tools.blob import Blob, BlobStorage
5
- from meshagent.tools import Toolkit, ToolContext, Tool
5
+ from meshagent.tools import Toolkit, ToolContext, Tool, BaseTool
6
6
  from meshagent.api.messaging import Response, LinkResponse, FileResponse, JsonResponse, TextResponse, EmptyResponse, RawOutputs, ensure_response
7
- from meshagent.api.schema_util import prompt_schema
8
7
  from meshagent.agents.adapter import ToolResponseAdapter, LLMAdapter
9
- from uuid import uuid4
10
8
  import json
11
- from jsonschema import validate
12
- from typing import List, Dict
13
-
14
- from openai import AsyncOpenAI, APIStatusError, NOT_GIVEN
15
- from openai.types.responses import ResponseFunctionToolCall, ResponseStreamEvent
16
-
17
- from copy import deepcopy
18
- from abc import abstractmethod, ABC
9
+ from typing import List, Literal
10
+ from meshagent.openai.proxy import get_client
11
+ from openai import AsyncOpenAI, APIStatusError, NOT_GIVEN, APIStatusError
12
+ from openai.types.responses import ResponseFunctionToolCall, ResponseComputerToolCall, ResponseStreamEvent, ResponseImageGenCallCompletedEvent
19
13
  import os
20
- import jsonschema
21
14
  from typing import Optional, Any, Callable
15
+ import base64
22
16
 
23
17
  import logging
24
18
  import re
25
19
  import asyncio
26
20
 
27
- logger = logging.getLogger("openai_agent")
21
+ from pydantic import BaseModel
28
22
 
23
+ logger = logging.getLogger("openai_agent")
29
24
 
25
+ from opentelemetry import trace
30
26
 
27
+ tracer = trace.get_tracer("openai.llm.responses")
31
28
 
32
29
 
33
30
  def _replace_non_matching(text: str, allowed_chars: str, replacement: str) -> str:
@@ -81,8 +78,18 @@ class ResponsesToolBundle:
81
78
 
82
79
  self._safe_names[name] = k
83
80
  self._tools_by_name[name] = v
81
+
82
+ if isinstance(v, OpenAIResponsesTool):
83
+
84
+ fns = v.get_open_ai_tool_definitions()
85
+ for fn in fns:
86
+ open_ai_tools.append(fn)
84
87
 
85
- if v.name != "computer_call":
88
+ elif isinstance(v, Tool):
89
+
90
+ strict = True
91
+ if hasattr(v, "strict"):
92
+ strict = getattr(v, "strict") == True
86
93
 
87
94
  fn = {
88
95
  "type" : "function",
@@ -91,10 +98,9 @@ class ResponsesToolBundle:
91
98
  "parameters" : {
92
99
  **v.input_schema,
93
100
  },
94
- "strict": True,
101
+ "strict": strict,
95
102
  }
96
103
 
97
-
98
104
  if v.defs != None:
99
105
  fn["parameters"]["$defs"] = v.defs
100
106
 
@@ -102,7 +108,7 @@ class ResponsesToolBundle:
102
108
 
103
109
  else:
104
110
 
105
- open_ai_tools.append(v.options)
111
+ raise RoomException(f"unsupported tool type {type(v)}")
106
112
 
107
113
  if len(open_ai_tools) == 0:
108
114
  open_ai_tools = None
@@ -134,7 +140,7 @@ class ResponsesToolBundle:
134
140
  logger.error("failed calling %s %s", tool_call.id, name, exc_info=e)
135
141
  raise
136
142
 
137
- def get_tool(self, name: str) -> Tool | None:
143
+ def get_tool(self, name: str) -> BaseTool | None:
138
144
  return self._tools_by_name.get(name, None)
139
145
 
140
146
  def contains(self, name: str) -> bool:
@@ -208,7 +214,7 @@ class OpenAIResponsesToolResponseAdapter(ToolResponseAdapter):
208
214
  if isinstance(response, RawOutputs):
209
215
 
210
216
  for output in response.outputs:
211
-
217
+
212
218
  room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : output })
213
219
 
214
220
  return response.outputs
@@ -219,15 +225,11 @@ class OpenAIResponsesToolResponseAdapter(ToolResponseAdapter):
219
225
  "output" : output,
220
226
  "call_id" : tool_call.call_id,
221
227
  "type" : "function_call_output"
222
- }
223
-
228
+ }
224
229
 
225
230
  room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : message })
226
-
227
- return [ message ]
228
-
229
-
230
231
 
232
+ return [ message ]
231
233
 
232
234
  class OpenAIResponsesAdapter(LLMAdapter[ResponsesToolBundle]):
233
235
  def __init__(self,
@@ -235,13 +237,15 @@ class OpenAIResponsesAdapter(LLMAdapter[ResponsesToolBundle]):
235
237
  parallel_tool_calls : Optional[bool] = None,
236
238
  client: Optional[AsyncOpenAI] = None,
237
239
  retries : int = 0,
238
- response_options : Optional[dict] = None
240
+ response_options : Optional[dict] = None,
241
+ provider: str = "openai"
239
242
  ):
240
243
  self._model = model
241
244
  self._parallel_tool_calls = parallel_tool_calls
242
245
  self._client = client
243
246
  self._retries = retries
244
247
  self._response_options = response_options
248
+ self._provider = provider
245
249
 
246
250
  def create_chat_context(self):
247
251
  system_role = "system"
@@ -262,7 +266,6 @@ class OpenAIResponsesAdapter(LLMAdapter[ResponsesToolBundle]):
262
266
  return context
263
267
 
264
268
  async def check_for_termination(self, *, context: AgentChatContext, room: RoomClient) -> bool:
265
-
266
269
  if len(context.previous_messages) > 0:
267
270
  last_message = context.previous_messages[-1]
268
271
  logger.info(f"last_message {last_message}")
@@ -276,289 +279,899 @@ class OpenAIResponsesAdapter(LLMAdapter[ResponsesToolBundle]):
276
279
 
277
280
  return True
278
281
 
279
- def _get_client(self, *, room: RoomClient) -> AsyncOpenAI:
280
- if self._client != None:
281
-
282
- openai = self._client
283
- else:
284
- token : str = room.protocol.token
285
- url : str = room.room_url
286
-
287
- room_proxy_url = f"{url}/v1"
288
-
289
- openai=AsyncOpenAI(
290
- api_key=token,
291
- base_url=room_proxy_url,
292
- default_headers={
293
- "Meshagent-Session" : room.session_id
294
- }
295
- )
296
-
297
- return openai
298
-
299
282
  # Takes the current chat context, executes a completion request and processes the response.
300
283
  # If a tool calls are requested, invokes the tools, processes the tool calls results, and appends the tool call results to the context
301
284
  async def next(self,
302
285
  *,
303
286
  context: AgentChatContext,
304
287
  room: RoomClient,
305
- toolkits: Toolkit,
288
+ toolkits: list[Toolkit],
306
289
  tool_adapter: Optional[ToolResponseAdapter] = None,
307
290
  output_schema: Optional[dict] = None,
308
291
  event_handler: Optional[Callable[[ResponseStreamEvent],None]] = None
309
- ):
310
- if tool_adapter == None:
311
- tool_adapter = OpenAIResponsesToolResponseAdapter()
312
-
313
- try:
314
-
315
- openai = self._get_client(room=room)
316
-
317
- response_schema = output_schema
318
- response_name = "response"
319
-
320
- while True:
292
+ ):
293
+ with tracer.start_as_current_span("llm.turn") as span:
321
294
 
322
- # We need to do this inside the loop because tools can change mid loop
323
- # for example computer use adds goto tools after the first interaction
324
- tool_bundle = ResponsesToolBundle(toolkits=[
325
- *toolkits,
326
- ])
327
- open_ai_tools = tool_bundle.to_json()
295
+ span.set_attributes({
296
+ "chat_context" : context.id,
297
+ "api" : "responses"
298
+ })
328
299
 
329
- if open_ai_tools != None:
330
- logger.info("OpenAI Tools: %s", json.dumps(open_ai_tools))
331
- else:
332
- logger.info("OpenAI Tools: Empty")
333
- open_ai_tools = NOT_GIVEN
300
+ if tool_adapter == None:
301
+ tool_adapter = OpenAIResponsesToolResponseAdapter()
334
302
 
303
+ try:
304
+
305
+ while True:
306
+
307
+ with tracer.start_as_current_span("llm.turn.iteration") as span:
335
308
 
336
-
337
- logger.info("model: %s, context: %s, output_schema: %s", self._model, context.messages, output_schema)
338
- ptc = self._parallel_tool_calls
339
- extra = {}
340
- if ptc != None and self._model.startswith("o") == False:
341
- extra["parallel_tool_calls"] = ptc
342
-
343
- text = NOT_GIVEN
344
- if output_schema != None:
345
- text = {
346
- "format" : {
347
- "type" : "json_schema",
348
- "name" : response_name,
349
- "schema" : response_schema,
350
- "strict" : True,
351
- }
352
- }
309
+ span.set_attributes({
310
+ "model": self._model,
311
+ "provider": self._provider
312
+ })
313
+
314
+ openai = get_client(room=room)
353
315
 
354
-
355
- previous_response_id = NOT_GIVEN
356
- if context.previous_response_id != None:
357
- previous_response_id = context.previous_response_id
358
-
359
- stream = event_handler != None
360
-
361
- for i in range(self._retries + 1):
362
- if range == self._retries:
363
- raise RoomException("exceeded maximum attempts calling openai")
364
- try:
365
- response_options = self._response_options
366
- if response_options == None:
367
- response_options = {}
368
-
369
- response : Response = await openai.responses.create(
370
- stream=stream,
371
- model = self._model,
372
- input = context.messages,
373
- tools = open_ai_tools,
374
- text = text,
375
- previous_response_id=previous_response_id,
376
-
377
- **response_options
378
- )
379
- break
380
- except Exception as e:
381
- logger.error(f"error calling openai attempt: {i+1}", exc_info=e)
382
- if i == self._retries:
383
- raise
384
-
316
+ response_schema = output_schema
317
+ response_name = "response"
385
318
 
386
- async def handle_message(message):
319
+ # We need to do this inside the loop because tools can change mid loop
320
+ # for example computer use adds goto tools after the first interaction
321
+ tool_bundle = ResponsesToolBundle(toolkits=[
322
+ *toolkits,
323
+ ])
324
+ open_ai_tools = tool_bundle.to_json()
387
325
 
388
- room.developer.log_nowait(type=f"llm.message", data={
389
- "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : message.to_dict()
390
- })
326
+ if open_ai_tools != None:
327
+ logger.info("OpenAI Tools: %s", json.dumps(open_ai_tools))
328
+ else:
329
+ logger.info("OpenAI Tools: Empty")
330
+ open_ai_tools = NOT_GIVEN
331
+
391
332
 
392
- if message.type == "function_call":
393
-
394
- tasks = []
333
+ logger.info("model: %s, context: %s, output_schema: %s", self._model, context.messages, output_schema)
334
+ ptc = self._parallel_tool_calls
335
+ extra = {}
336
+ if ptc != None and self._model.startswith("o") == False:
337
+ extra["parallel_tool_calls"] = ptc
338
+ span.set_attribute("parallel_tool_calls", ptc)
339
+ else:
340
+ span.set_attribute("parallel_tool_calls", False)
341
+
342
+ text = NOT_GIVEN
343
+ if output_schema != None:
344
+ span.set_attribute("response_format", "json_schema")
345
+ text = {
346
+ "format" : {
347
+ "type" : "json_schema",
348
+ "name" : response_name,
349
+ "schema" : response_schema,
350
+ "strict" : True,
351
+ }
352
+ }
353
+ else:
354
+ span.set_attribute("response_format", "text")
395
355
 
396
- async def do_tool_call(tool_call: ResponseFunctionToolCall):
356
+
357
+ previous_response_id = NOT_GIVEN
358
+ if context.previous_response_id != None:
359
+ previous_response_id = context.previous_response_id
360
+
361
+ stream = event_handler != None
362
+
363
+ for i in range(self._retries + 1):
364
+
365
+ if range == self._retries:
366
+ raise RoomException("exceeded maximum attempts calling openai")
397
367
  try:
398
- tool_context = ToolContext(
399
- room=room,
400
- caller=room.local_participant,
401
- caller_context={ "chat" : context.to_json }
402
- )
403
- tool_response = await tool_bundle.execute(context=tool_context, tool_call=tool_call)
404
- if tool_response.caller_context != None:
405
- if tool_response.caller_context.get("chat", None) != None:
406
- tool_chat_context = AgentChatContext.from_json(tool_response.caller_context["chat"])
407
- if tool_chat_context.previous_response_id != None:
408
- context.track_response(tool_chat_context.previous_response_id)
409
-
410
- logger.info(f"tool response {tool_response}")
411
- return await tool_adapter.create_messages(context=context, tool_call=tool_call, room=room, response=tool_response)
368
+ with tracer.start_as_current_span("llm.invoke") as span:
369
+ response_options = self._response_options
370
+ if response_options == None:
371
+ response_options = {}
372
+
373
+ response : Response = await openai.responses.create(
374
+ stream=stream,
375
+ model = self._model,
376
+ input = context.messages,
377
+ tools = open_ai_tools,
378
+ text = text,
379
+ previous_response_id=previous_response_id,
380
+
381
+ **response_options
382
+ )
383
+ break
384
+ except APIStatusError as e:
385
+ logger.error(f"error calling openai attempt: {i+1} ({e.response.request.url})", exc_info=e)
386
+ raise
412
387
  except Exception as e:
413
- logger.error(f"unable to complete tool call {tool_call}", exc_info=e)
414
- room.developer.log_nowait(type="llm.error", data={ "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "error" : f"{e}" })
415
-
416
- return [{
417
- "output" : json.dumps({"error":f"unable to complete tool call: {e}"}),
418
- "call_id" : tool_call.call_id,
419
- "type" : "function_call_output"
420
- }]
388
+ logger.error(f"error calling openai attempt: {i+1}", exc_info=e)
389
+ if i == self._retries:
390
+ raise
391
+
421
392
 
393
+ async def handle_message(message: BaseModel):
422
394
 
423
- tasks.append(asyncio.create_task(do_tool_call(message)))
424
395
 
425
- results = await asyncio.gather(*tasks)
426
396
 
427
- all_results = []
428
- for result in results:
429
- room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : result })
430
- all_results.extend(result)
397
+ with tracer.start_as_current_span("llm.handle_response") as span:
431
398
 
432
- return all_results, False
399
+ span.set_attributes({
400
+ "type" : message.type,
401
+ "message" : message.model_dump_json()
402
+ })
433
403
 
434
- elif message.type == "computer_call" and tool_bundle.get_tool("computer_call"):
435
- tool_context = ToolContext(
436
- room=room,
437
- caller=room.local_participant,
438
- caller_context={ "chat" : context.to_json }
439
- )
440
- outputs = (await tool_bundle.get_tool("computer_call").execute(context=tool_context, arguments=message.to_dict(mode="json"))).outputs
404
+ room.developer.log_nowait(type=f"llm.message", data={
405
+ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : message.to_dict()
406
+ })
441
407
 
442
- return outputs, False
443
-
444
- elif message.type == "reasoning":
445
- reasoning = tool_bundle.get_tool("reasoning_tool")
446
- if reasoning != None:
447
- await tool_bundle.get_tool("reasoning_tool").execute(context=tool_context, arguments=message.to_dict(mode="json"))
448
-
449
- elif message.type == "message":
450
-
451
- contents = message.content
452
- if response_schema == None:
453
- return [], False
454
- else:
455
- for content in contents:
456
- # First try to parse the result
457
- try:
458
- full_response = json.loads(content.text)
459
-
460
- # sometimes open ai packs two JSON chunks seperated by newline, check if that's why we couldn't parse
461
- except json.decoder.JSONDecodeError as e:
462
- for part in content.text.splitlines():
463
- if len(part.strip()) > 0:
464
- full_response = json.loads(part)
465
-
408
+ if message.type == "function_call":
409
+
410
+ tasks = []
411
+
412
+ async def do_tool_call(tool_call: ResponseFunctionToolCall):
413
+
466
414
  try:
467
- self.validate(response=full_response, output_schema=response_schema)
415
+ with tracer.start_as_current_span("llm.handle_tool_call") as span:
416
+
417
+ span.set_attributes({
418
+ "id": tool_call.id,
419
+ "name": tool_call.name,
420
+ "call_id": tool_call.call_id,
421
+ "arguments": json.dumps(tool_call.arguments)
422
+ })
423
+
424
+ tool_context = ToolContext(
425
+ room=room,
426
+ caller=room.local_participant,
427
+ caller_context={ "chat" : context.to_json() }
428
+ )
429
+ tool_response = await tool_bundle.execute(context=tool_context, tool_call=tool_call)
430
+ if tool_response.caller_context != None:
431
+ if tool_response.caller_context.get("chat", None) != None:
432
+ tool_chat_context = AgentChatContext.from_json(tool_response.caller_context["chat"])
433
+ if tool_chat_context.previous_response_id != None:
434
+ context.track_response(tool_chat_context.previous_response_id)
435
+
436
+ span.set_attribute("response", await tool_adapter.to_plain_text(room=room, response=tool_response))
437
+
438
+ logger.info(f"tool response {tool_response}")
439
+ return await tool_adapter.create_messages(context=context, tool_call=tool_call, room=room, response=tool_response)
440
+
468
441
  except Exception as e:
469
- logger.error("recieved invalid response, retrying", exc_info=e)
470
- error = { "role" : "user", "content" : "encountered a validation error with the output: {error}".format(error=e)}
471
- room.developer.log_nowait(type="llm.message", data={ "context" : message.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : error })
472
- context.messages.append(error)
473
- continue
442
+ logger.error(f"unable to complete tool call {tool_call}", exc_info=e)
443
+ room.developer.log_nowait(type="llm.error", data={ "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "error" : f"{e}" })
474
444
 
475
- return [ full_response ], True
476
- else:
477
- raise RoomException("Unexpected response from OpenAI {response}".format(response=message))
478
-
479
- return [], False
480
-
481
- if stream == False:
482
- room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "response" : response.to_dict() })
483
-
484
- context.track_response(response.id)
445
+ return [{
446
+ "output" : json.dumps({"error":f"unable to complete tool call: {e}"}),
447
+ "call_id" : tool_call.call_id,
448
+ "type" : "function_call_output"
449
+ }]
450
+
451
+
452
+ tasks.append(asyncio.create_task(do_tool_call(message)))
453
+
454
+ results = await asyncio.gather(*tasks)
455
+
456
+ all_results = []
457
+ for result in results:
458
+ room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : result })
459
+ all_results.extend(result)
460
+
461
+ return all_results, False
462
+
463
+ elif message.type == "message":
464
+ with tracer.start_as_current_span("llm.handle_message") as span:
465
+
466
+ contents = message.content
467
+ if response_schema == None:
468
+ return [], False
469
+ else:
470
+ for content in contents:
471
+ # First try to parse the result
472
+ try:
473
+ full_response = json.loads(content.text)
474
+
475
+ # sometimes open ai packs two JSON chunks seperated by newline, check if that's why we couldn't parse
476
+ except json.decoder.JSONDecodeError as e:
477
+ for part in content.text.splitlines():
478
+ if len(part.strip()) > 0:
479
+ full_response = json.loads(part)
480
+
481
+ try:
482
+ self.validate(response=full_response, output_schema=response_schema)
483
+ except Exception as e:
484
+ logger.error("recieved invalid response, retrying", exc_info=e)
485
+ error = { "role" : "user", "content" : "encountered a validation error with the output: {error}".format(error=e)}
486
+ room.developer.log_nowait(type="llm.message", data={ "context" : message.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "message" : error })
487
+ context.messages.append(error)
488
+ continue
489
+
490
+ return [ full_response ], True
491
+ #elif message.type == "computer_call" and tool_bundle.get_tool("computer_call"):
492
+ # with tracer.start_as_current_span("llm.handle_computer_call") as span:
493
+ #
494
+ # computer_call :ResponseComputerToolCall = message
495
+ # span.set_attributes({
496
+ # "id": computer_call.id,
497
+ # "action": computer_call.action,
498
+ # "call_id": computer_call.call_id,
499
+ # "type": json.dumps(computer_call.type)
500
+ # })
501
+
502
+ # tool_context = ToolContext(
503
+ # room=room,
504
+ # caller=room.local_participant,
505
+ # caller_context={ "chat" : context.to_json }
506
+ # )
507
+ # outputs = (await tool_bundle.get_tool("computer_call").execute(context=tool_context, arguments=message.to_dict(mode="json"))).outputs
508
+
509
+ # return outputs, False
510
+
511
+
512
+ else:
513
+ for toolkit in toolkits:
514
+ for tool in toolkit.tools:
515
+ if isinstance(tool, OpenAIResponsesTool):
516
+ handlers = tool.get_open_ai_output_handlers()
517
+ if message.type in handlers:
518
+ tool_context = ToolContext(
519
+ room=room,
520
+ caller=room.local_participant,
521
+ caller_context={ "chat" : context.to_json() }
522
+ )
523
+ result = await handlers[message.type](tool_context, **message.to_dict(mode="json"))
524
+ if result != None:
525
+ return [ result ], False
526
+ else:
527
+
528
+ logger.warning(f"OpenAI response handler was not registered for {message.type}")
529
+
530
+
531
+ return [], False
532
+
533
+ if stream == False:
534
+ room.developer.log_nowait(type="llm.message", data={ "context" : context.id, "participant_id" : room.local_participant.id, "participant_name" : room.local_participant.get_attribute("name"), "response" : response.to_dict() })
535
+
536
+ context.track_response(response.id)
537
+
538
+ final_outputs = []
539
+
540
+ for message in response.output:
541
+ context.previous_messages.append(message.to_dict())
542
+ outputs, done = await handle_message(message=message)
543
+ if done:
544
+ final_outputs.extend(outputs)
545
+ else:
546
+ for output in outputs:
547
+ context.messages.append(output)
548
+
549
+ if len(final_outputs) > 0:
550
+
551
+ return final_outputs[0]
552
+
553
+ with tracer.start_as_current_span("llm.turn.check_for_termination") as span:
554
+
555
+ term = await self.check_for_termination(context=context, room=room)
556
+ if term:
557
+ span.set_attribute("terminate", True)
558
+ text = ""
559
+ for output in response.output:
560
+ if output.type == "message":
561
+ for content in output.content:
562
+ text += content.text
563
+
564
+ return text
565
+ else:
566
+ span.set_attribute("terminate", False)
567
+
485
568
 
486
- final_outputs = []
487
-
488
-
489
- for message in response.output:
490
- context.previous_messages.append(message.to_dict())
491
- outputs, done = await handle_message(message=message)
492
- if done:
493
- final_outputs.extend(outputs)
494
569
  else:
495
- for output in outputs:
496
- context.messages.append(output)
570
+
571
+ final_outputs = []
572
+ all_outputs = []
573
+ async for e in response:
574
+ with tracer.start_as_current_span("llm.stream.event") as span:
497
575
 
498
- if len(final_outputs) > 0:
576
+ event : ResponseStreamEvent = e
577
+ span.set_attributes({
578
+ "type" : event.type,
579
+ "event" : event.model_dump_json()
580
+ })
499
581
 
500
- return final_outputs[0]
501
-
502
- term = await self.check_for_termination(context=context, room=room)
503
- if term:
504
- text = ""
505
- for output in response.output:
506
- if output.type == "message":
507
- for content in output.content:
508
- text += content.text
582
+ event_handler(event)
509
583
 
510
- return text
584
+ if event.type == "response.completed":
511
585
 
586
+
587
+ context.track_response(event.response.id)
588
+
589
+ context.messages.extend(all_outputs)
512
590
 
513
- else:
514
-
515
- final_outputs = []
516
- all_outputs = []
517
- async for e in response:
518
-
519
- event : ResponseStreamEvent = e
591
+ with tracer.start_as_current_span("llm.turn.check_for_termination") as span:
592
+ term = await self.check_for_termination(context=context, room=room)
593
+
594
+ if term:
595
+ span.set_attribute("terminate", True)
596
+
597
+ text = ""
598
+ for output in event.response.output:
599
+ if output.type == "message":
600
+ for content in output.content:
601
+ text += content.text
520
602
 
521
- event_handler(event)
603
+ return text
522
604
 
523
- if event.type == "response.completed":
524
- context.track_response(event.response.id)
525
-
526
- context.messages.extend(all_outputs)
605
+ span.set_attribute("terminate", False)
527
606
 
528
- term = await self.check_for_termination(context=context, room=room)
529
- if term:
530
- text = ""
531
- for output in event.response.output:
532
- if output.type == "message":
533
- for content in output.content:
534
- text += content.text
535
607
 
536
- return text
608
+ all_outputs = []
537
609
 
610
+ elif event.type == "response.output_item.done":
611
+
612
+ context.previous_messages.append(event.item.to_dict())
613
+
614
+ outputs, done = await handle_message(message=event.item)
615
+ if done:
616
+ final_outputs.extend(outputs)
617
+ else:
618
+ for output in outputs:
619
+ all_outputs.append(output)
538
620
 
539
- all_outputs = []
621
+ else:
622
+ for toolkit in toolkits:
623
+ for tool in toolkit.tools:
540
624
 
541
- elif event.type == "response.output_item.done":
625
+ if isinstance(tool, OpenAIResponsesTool):
542
626
 
543
- context.previous_messages.append(event.item.to_dict())
544
-
545
- outputs, done = await handle_message(message=event.item)
546
- if done:
547
- final_outputs.extend(outputs)
548
- else:
549
- for output in outputs:
550
- all_outputs.append(output)
627
+ callbacks = tool.get_open_ai_stream_callbacks()
628
+
629
+ if event.type in callbacks:
630
+
631
+ tool_context = ToolContext(
632
+ room=room,
633
+ caller=room.local_participant,
634
+ caller_context={ "chat" : context.to_json() }
635
+ )
636
+
637
+ await callbacks[event.type](tool_context, **event.to_dict())
551
638
 
552
- if len(final_outputs) > 0:
553
639
 
554
- return final_outputs[0]
640
+ if len(final_outputs) > 0:
555
641
 
642
+ return final_outputs[0]
643
+
644
+ except APIStatusError as e:
645
+ raise RoomException(f"Error from OpenAI: {e}")
556
646
 
557
-
558
-
559
- except APIStatusError as e:
560
- raise RoomException(f"Error from OpenAI: {e}")
647
+
648
+ class OpenAIResponsesTool(BaseTool):
649
+
650
+ def get_open_ai_tool_definitions(self) -> list[dict]:
651
+ return []
652
+
653
+ def get_open_ai_stream_callbacks(self) -> dict[str, Callable]:
654
+ return {}
655
+
656
+ def get_open_ai_output_handlers(self) -> dict[str, Callable]:
657
+ return {}
658
+
659
+
660
+ class ImageGenerationTool(OpenAIResponsesTool):
661
+ def __init__(self, *,
662
+ background: Literal["transparent","opaque","auto"] = None,
663
+ input_image_mask_url: Optional[str] = None,
664
+ model: Optional[str] = None,
665
+ moderation: Optional[str] = None,
666
+ output_compression: Optional[int] = None,
667
+ output_format: Optional[Literal["png","webp","jpeg"]] = None,
668
+ partial_images: Optional[int] = None,
669
+ quality: Optional[Literal["auto", "low", "medium", "high"]] = None,
670
+ size: Optional[Literal["1024x1024","1024x1536","1536x1024","auto"]] = None
671
+ ):
672
+ super().__init__(name="image_generation")
673
+ self.background = background
674
+ self.input_image_mask_url = input_image_mask_url
675
+ self.model = model
676
+ self.moderation = moderation
677
+ self.output_compression = output_compression
678
+ self.output_format = output_format
679
+ if partial_images == None:
680
+ partial_images = 1 # streaming wants non zero, and we stream by default
681
+ self.partial_images = partial_images
682
+ self.quality = quality
683
+ self.size = size
684
+
685
+ def get_open_ai_tool_definitions(self):
686
+ opts = {
687
+ "type" : "image_generation"
688
+ }
689
+
690
+ if self.background != None:
691
+ opts["background"] = self.background
692
+
693
+ if self.input_image_mask_url != None:
694
+ opts["input_image_mask"] = { "image_url" : self.input_image_mask_url }
695
+
696
+ if self.model != None:
697
+ opts["model"] = self.model
698
+
699
+ if self.moderation != None:
700
+ opts["moderation"] = self.moderation
701
+
702
+ if self.output_compression != None:
703
+ opts["output_compression"] = self.output_compression
704
+
705
+ if self.output_format != None:
706
+ opts["output_format"] = self.output_format
707
+
708
+ if self.partial_images != None:
709
+ opts["partial_images"] = self.partial_images
710
+
711
+ if self.quality != None:
712
+ opts["quality"] = self.quality
713
+
714
+ if self.size != None:
715
+ opts["size"] = self.size
716
+
717
+ return [ opts ]
718
+
719
+ def get_open_ai_stream_callbacks(self):
720
+ return {
721
+ "response.image_generation_call.completed" : self.on_image_generation_completed,
722
+ "response.image_generation_call.in_progress" : self.on_image_generation_in_progress,
723
+ "response.image_generation_call.generating" : self.on_image_generation_generating,
724
+ "response.image_generation_call.partial_image" : self.on_image_generation_partial,
725
+ }
726
+
727
+ def get_open_ai_output_handlers(self):
728
+ return {
729
+ "image_generation_call" : self.handle_image_generated
730
+ }
731
+
732
+ # response.image_generation_call.completed
733
+ async def on_image_generation_completed(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
734
+ pass
735
+
736
+ # response.image_generation_call.in_progress
737
+ async def on_image_generation_in_progress(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
738
+ pass
739
+
740
+ # response.image_generation_call.generating
741
+ async def on_image_generation_generating(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
742
+ pass
743
+
744
+ # response.image_generation_call.partial_image
745
+ async def on_image_generation_partial(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, partial_image_b64: str, partial_image_index: int, size: str, quality: str, background: str, output_format: str, **extra):
746
+ pass
747
+
748
+ async def on_image_generated(self, context: ToolContext, *, item_id: str, data: bytes, status: str, size: str, quality: str, background: str, output_format: str, **extra):
749
+ pass
750
+
751
+ async def handle_image_generated(self, context: ToolContext, *, id: str, result: str | None, status: str, type: str, size: str, quality: str, background: str, output_format: str, **extra):
752
+ if result != None:
753
+ data = base64.b64decode(result)
754
+ await self.on_image_generated(context, item_id=id, data=data, status=status, size=size, quality=quality, background=background, output_format=output_format)
755
+
756
+
757
+ class LocalShellTool(OpenAIResponsesTool):
758
+ def __init__(self):
759
+ super().__init__(name="local_shell")
760
+
761
+ def get_open_ai_tool_definitions(self):
762
+ return [
763
+ {
764
+ "type" : "local_shell"
765
+ }
766
+ ]
767
+
768
+ def get_open_ai_output_handlers(self):
769
+ return {
770
+ "local_shell_call" : self.handle_local_shell_call
771
+ }
772
+
773
+ async def execute_shell_command(self, context: ToolContext, *, command: list[str], env: dict, type: str, timeout_ms: int | None = None, user: str | None = None, working_directory: str | None = None):
774
+
775
+ merged_env = {**os.environ, **(env or {})}
776
+
777
+ # Spawn the process
778
+ proc = await asyncio.create_subprocess_exec(
779
+ *(command if isinstance(command, (list, tuple)) else [command]),
780
+ cwd=working_directory or os.getcwd(),
781
+ env=merged_env,
782
+ stdout=asyncio.subprocess.PIPE,
783
+ stderr=asyncio.subprocess.PIPE,
784
+ )
785
+
786
+ try:
787
+ stdout, stderr = await asyncio.wait_for(
788
+ proc.communicate(),
789
+ timeout=timeout_ms / 1000 if timeout_ms else None,
790
+ )
791
+ except asyncio.TimeoutError:
792
+ proc.kill() # send SIGKILL / TerminateProcess
793
+ stdout, stderr = await proc.communicate()
794
+ raise # re-raise so caller sees the timeout
795
+
796
+
797
+ encoding = os.device_encoding(1) or "utf-8"
798
+ stdout = stdout.decode(encoding, errors="replace")
799
+ stderr = stderr.decode(encoding, errors="replace")
800
+
801
+ return stdout + stderr
802
+
803
+ async def handle_local_shell_call(self, context, *, id: str, action: dict, call_id: str, status: str, type: str, **extra):
804
+
805
+ result = await self.execute_shell_command(context, **action)
806
+
807
+ output_item = {
808
+ "type": "local_shell_call_output",
809
+ "call_id": call_id,
810
+ "output": result,
811
+ }
812
+
813
+ return output_item
814
+
815
+
816
+ class ContainerFile:
817
+ def __init__(self, *, file_id: str, mime_type: str, container_id: str):
818
+ self.file_id = file_id
819
+ self.mime_type = mime_type
820
+ self.container_id = container_id
821
+
822
+ class CodeInterpreterTool(OpenAIResponsesTool):
823
+ def __init__(self, *, container_id: Optional[str] = None, file_ids: Optional[List[str]] = None):
824
+ super().__init__(name="code_interpreter_call")
825
+ self.container_id = container_id
826
+ self.file_ids = file_ids
827
+
828
+ def get_open_ai_tool_definitions(self):
829
+ opts = {
830
+ "type" : "code_interpreter"
831
+ }
832
+
833
+ if self.container_id != None:
834
+ opts["container_id"] = self.container_id
835
+
836
+ if self.file_ids != None:
837
+ if self.container_id != None:
838
+ raise Exception("Cannot specify both an existing container and files to upload in a code interpreter tool")
839
+
840
+ opts["container"] = {
841
+ "type" : "auto",
842
+ "file_ids" : self.file_ids
843
+ }
844
+
845
+ return [
846
+ opts
847
+ ]
848
+
849
+ def get_open_ai_output_handlers(self):
850
+ return {
851
+ "code_interpreter_call" : self.handle_code_interpreter_call
852
+ }
853
+
854
+ async def on_code_interpreter_result(self, context: ToolContext, *, code: str, logs: list[str], files: list[ContainerFile]):
855
+ pass
856
+
857
+ async def handle_code_interpreter_call(self, context, *, code: str, id: str, results: list[dict], call_id: str, status: str, type: str, container_id: str, **extra):
858
+
859
+ logs = []
860
+ files = []
861
+
862
+ for result in results:
863
+
864
+ if result.type == "logs":
865
+
866
+ logs.append(results["logs"])
867
+
868
+ elif result.type == "files":
869
+
870
+ files.append(ContainerFile(container_id=container_id, file_id=result["file_id"], mime_type=result["mime_type"]))
871
+
872
+ await self.on_code_interpreter_result(context, code=code, logs=logs, files=files)
873
+
874
+
875
+ class MCPToolDefinition:
876
+ def __init__(self, *, input_schema: dict, name: str, annotations: dict | None, description: str | None):
877
+ self.input_schema = input_schema
878
+ self.name = name
879
+ self.annotations = annotations
880
+ self.description = description
881
+
882
+ class MCPServer:
883
+ def __init__(self, *,
884
+ server_label: str,
885
+ server_url: str,
886
+ allowed_tools: Optional[list[str]] = None,
887
+ headers: Optional[dict] = None,
888
+ # require approval for all tools
889
+ require_approval: Optional[Literal["always","never"]] = None,
890
+ # list of tools that always require approval
891
+ always_require_approval: Optional[list[str]] = None,
892
+ # list of tools that never require approval
893
+ never_require_approval: Optional[list[str]] = None
894
+ ):
895
+ self.server_label = server_label
896
+ self.server_url = server_url
897
+ self.allowed_tools = allowed_tools
898
+ self.headers = headers
899
+ self.require_approval = require_approval
900
+ self.always_require_approval = always_require_approval
901
+ self.never_require_approval = never_require_approval
902
+
903
+
904
+ class MCPTool(OpenAIResponsesTool):
905
+ def __init__(self, *,
906
+ servers: list[MCPServer]
907
+ ):
908
+ super().__init__(name="mcp")
909
+ self.servers = servers
910
+
911
+
912
+ def get_open_ai_tool_definitions(self):
913
+
914
+ defs = []
915
+ for server in self.servers:
916
+ opts = {
917
+ "type" : "mcp",
918
+ "server_label" : server.server_label,
919
+ "server_url" : server.server_url,
920
+ }
921
+
922
+ if server.allowed_tools != None:
923
+ opts["allowed_tools"] = server.allowed_tools
924
+
925
+ if server.headers != None:
926
+ opts["headers"] = server.headers
927
+
928
+
929
+ if server.always_require_approval != None or server.never_require_approval != None:
930
+ opts["require_approval"] = {}
931
+
932
+ if server.always_require_approval != None:
933
+ opts["require_approval"]["always"] = { "tool_names" : server.always_require_approval }
561
934
 
935
+ if server.never_require_approval != None:
936
+ opts["require_approval"]["never"] = { "tool_names" : server.never_require_approval }
937
+
938
+ if server.require_approval:
939
+ opts["require_approval"] = server.require_approval
940
+
941
+ defs.append(opts)
942
+
943
+ return defs
944
+
945
+ def get_open_ai_stream_callbacks(self):
946
+ return {
947
+ "response.mcp_list_tools.in_progress" : self.on_mcp_list_tools_in_progress,
948
+ "response.mcp_list_tools.failed" : self.on_mcp_list_tools_failed,
949
+ "response.mcp_list_tools.completed" : self.on_mcp_list_tools_completed,
950
+ "response.mcp_call.in_progress" : self.on_mcp_call_in_progress,
951
+ "response.mcp_call.failed" : self.on_mcp_call_failed,
952
+ "response.mcp_call.completed" : self.on_mcp_call_completed,
953
+ "response.mcp_call.arguments.done" : self.on_mcp_call_arguments_done,
954
+ "response.mcp_call.arguments.delta" : self.on_mcp_call_arguments_delta,
955
+ }
956
+
957
+ async def on_mcp_list_tools_in_progress(self, context: ToolContext, *, sequence_number: int, type: str, **extra):
958
+ pass
959
+
960
+ async def on_mcp_list_tools_failed(self, context: ToolContext, *, sequence_number: int, type: str, **extra):
961
+ pass
962
+
963
+ async def on_mcp_list_tools_completed(self, context: ToolContext, *, sequence_number: int, type: str, **extra):
964
+ pass
965
+
966
+ async def on_mcp_call_in_progress(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
967
+ pass
968
+
969
+ async def on_mcp_call_failed(self, context: ToolContext, *, sequence_number: int, type: str, **extra):
970
+ pass
971
+
972
+ async def on_mcp_call_completed(self, context: ToolContext, *, sequence_number: int, type: str, **extra):
973
+ pass
974
+
975
+ async def on_mcp_call_arguments_done(self, context: ToolContext, *, arguments: dict, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
976
+ pass
977
+
978
+ async def on_mcp_call_arguments_delta(self, context: ToolContext, *, delta: dict, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
979
+ pass
980
+
981
+
982
+ def get_open_ai_output_handlers(self):
983
+ return {
984
+ "mcp_call" : self.handle_mcp_call,
985
+ "mcp_list_tools" : self.handle_mcp_list_tools,
986
+ "mcp_approval_request" : self.handle_mcp_approval_request,
987
+ }
988
+
989
+ async def on_mcp_list_tools(self, context: ToolContext, *, server_label: str, tools: list[MCPToolDefinition], error: str | None, **extra):
990
+ pass
991
+
992
+ async def handle_mcp_list_tools(self, context, *, id: str, server_label: str, tools: list, type: str, error: str | None = None, **extra):
993
+
994
+ mcp_tools = []
995
+ for tool in tools:
996
+ mcp_tools.append(MCPToolDefinition(input_schema=tool["input_schema"], name=tool["name"], annotations=tool["annotations"], description=tool["description"]))
997
+
998
+ await self.on_mcp_list_tools(context, server_label=server_label, tools=mcp_tools, error=error)
999
+
1000
+
1001
+ async def on_mcp_call(self, context: ToolContext, *, name: str, arguments: str, server_label: str, error: str | None, output: str | None, **extra):
1002
+ pass
1003
+
1004
+ async def handle_mcp_call(self, context, *, arguments: str, id: str, name: str, server_label: str, type: str, error: str | None, output: str | None, **extra):
1005
+
1006
+ await self.on_mcp_call(context, name=name, arguments=arguments, server_label=server_label, error=error, output=output)
1007
+
1008
+
1009
+ async def on_mcp_approval_request(self, context: ToolContext, *, name: str, arguments: str, server_label: str, **extra) -> bool:
1010
+ return True
1011
+
1012
+ async def handle_mcp_approval_request(self, context: ToolContext, *, arguments: str, id: str, name: str, server_label: str, type: str, **extra):
1013
+ logger.info("approval requested for MCP tool {server_label}.{name}")
1014
+ should_approve = await self.on_mcp_approval_request(context, arguments=arguments, name=name, server_label=server_label)
1015
+ if should_approve:
1016
+ logger.info("approval granted for MCP tool {server_label}.{name}")
1017
+ return {
1018
+ "type": "mcp_approval_response",
1019
+ "approve": True,
1020
+ "approval_request_id": id
1021
+ }
1022
+ else:
1023
+ logger.info("approval denied for MCP tool {server_label}.{name}")
1024
+ return {
1025
+ "type": "mcp_approval_response",
1026
+ "approve": False,
1027
+ "approval_request_id": id
1028
+ }
1029
+
1030
+
1031
+ class ReasoningTool(OpenAIResponsesTool):
1032
+ def __init__(self):
1033
+ super().__init__(name="reasoning")
1034
+
1035
+
1036
+ def get_open_ai_output_handlers(self):
1037
+ return {
1038
+ "reasoning" : self.handle_reasoning,
1039
+ }
1040
+
1041
+ def get_open_ai_stream_callbacks(self):
1042
+ return {
1043
+ "response.reasoning_summary_text.done" : self.on_reasoning_summary_text_done,
1044
+ "response.reasoning_summary_text.delta" : self.on_reasoning_summary_text_delta,
1045
+ "response.reasoning_summary_part.done" : self.on_reasoning_summary_part_done,
1046
+ "response.reasoning_summary_part.added" : self.on_reasoning_summary_part_added,
1047
+ }
1048
+
1049
+ async def on_reasoning_summary_part_added(self, context: ToolContext, *, item_id: str, output_index: int, part: dict, sequence_number: int, summary_index: int, text: str, type: str, **extra):
1050
+ pass
1051
+
1052
+
1053
+ async def on_reasoning_summary_part_done(self, context: ToolContext, *, item_id: str, output_index: int, part: dict, sequence_number: int, summary_index: int, text: str, type: str, **extra):
1054
+ pass
1055
+
1056
+ async def on_reasoning_summary_text_delta(self, context: ToolContext, *, delta: str, output_index: int, sequence_number: int, summary_index: int, text: str, type: str, **extra):
1057
+ pass
1058
+
1059
+ async def on_reasoning_summary_text_done(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, summary_index: int, text: str, type: str, **extra):
1060
+ pass
1061
+
1062
+ async def on_reasoning(self, context: ToolContext, *, summary: str, encrypted_content: str | None, status: Literal["in_progress", "completed", "incomplete"]):
1063
+ pass
1064
+
1065
+ async def handle_reasoning(self, context: ToolContext, *, id: str, summary: str, type: str, encrypted_content: str | None, status: str, **extra):
1066
+
1067
+ await self.on_reasoning(context, summary=summary, encrypted_content=encrypted_content, status=status)
1068
+
1069
+
1070
+ # TODO: computer tool call
1071
+
1072
+ class WebSearchTool(OpenAIResponsesTool):
1073
+ def __init__(self):
1074
+ super().__init__(name="web_search")
1075
+
1076
+
1077
+ def get_open_ai_tool_definitions(self) -> list[dict]:
1078
+ return [
1079
+ {
1080
+ "type" : "web_search_preview"
1081
+ }
1082
+ ]
1083
+
1084
+
1085
+ def get_open_ai_stream_callbacks(self):
1086
+ return {
1087
+ "response.web_search_call.in_progress" : self.on_web_search_call_in_progress,
1088
+ "response.web_search_call.searching" : self.on_web_search_call_searching,
1089
+ "response.web_search_call.completed" : self.on_web_search_call_completed,
1090
+ }
1091
+
1092
+ def get_open_ai_output_handlers(self):
1093
+ return {
1094
+ "web_search_call" : self.handle_web_search_call
1095
+ }
1096
+
1097
+ async def on_web_search_call_in_progress(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1098
+ pass
1099
+
1100
+ async def on_web_search_call_searching(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1101
+ pass
1102
+
1103
+ async def on_web_search_call_completed(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1104
+ pass
1105
+
1106
+ async def on_web_search(self, context: ToolContext, *, status: str, **extra):
1107
+ pass
1108
+
1109
+ async def handle_web_search_call(self, context: ToolContext, *, id: str, status: str, type: str, **extra):
1110
+
1111
+ await self.on_web_search(context, status=status)
1112
+
1113
+ class FileSearchResult:
1114
+ def __init__(self, *, attributes: dict, file_id: str, filename: str, score: float, text: str):
1115
+ self.attributes = attributes
1116
+ self.file_id = file_id
1117
+ self.filename = filename
1118
+ self.score = score
1119
+ self.text = text
1120
+
1121
+ class FileSearchTool(OpenAIResponsesTool):
1122
+ def __init__(self, *, vector_store_ids: list[str], filters: Optional[dict] = None, max_num_results: Optional[int] = None, ranking_options: Optional[dict] = None):
1123
+ super().__init__(name="file_search")
1124
+
1125
+ self.vector_store_ids = vector_store_ids
1126
+ self.filters = filters
1127
+ self.max_num_results = max_num_results
1128
+ self.ranking_options = ranking_options
1129
+
1130
+ def get_open_ai_tool_definitions(self) -> list[dict]:
1131
+ return [
1132
+ {
1133
+ "type" : "file_search",
1134
+ "vector_store_ids" : self.vector_store_ids,
1135
+ "filters" : self.filters,
1136
+ "max_num_results" : self.max_num_results,
1137
+ "ranking_options" : self.ranking_options
1138
+ }
1139
+ ]
1140
+
1141
+
1142
+ def get_open_ai_stream_callbacks(self):
1143
+ return {
1144
+ "response.file_search_call.in_progress" : self.on_file_search_call_in_progress,
1145
+ "response.file_search_call.searching" : self.on_file_search_call_searching,
1146
+ "response.file_search_call.completed" : self.on_file_search_call_completed,
1147
+ }
1148
+
1149
+
1150
+ def get_open_ai_output_handlers(self):
1151
+ return {
1152
+ "handle_file_search_call" : self.handle_file_search_call
1153
+ }
1154
+
1155
+
1156
+ async def on_file_search_call_in_progress(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1157
+ pass
1158
+
1159
+ async def on_file_search_call_searching(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1160
+ pass
1161
+
1162
+ async def on_file_search_call_completed(self, context: ToolContext, *, item_id: str, output_index: int, sequence_number: int, type: str, **extra):
1163
+ pass
1164
+
1165
+ async def on_file_search(self, context: ToolContext, *, queries: list, results: list[FileSearchResult], status: Literal["in_progress", "searching", "incomplete", "failed"]):
1166
+ pass
1167
+
1168
+ async def handle_file_search_call(self, context: ToolContext, *, id: str, queries: list, status: str, results: dict | None, type: str, **extra):
1169
+
1170
+ search_results = None
1171
+ if results != None:
1172
+ search_results = []
1173
+ for result in results:
1174
+ search_results.append(FileSearchResult(**result))
562
1175
 
1176
+ await self.on_file_search(context, queries=queries, results=search_results, status=status)
563
1177
 
564
-