meshagent-openai 0.0.29__py3-none-any.whl → 0.0.31__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.
- meshagent/openai/proxy/__init__.py +1 -0
- meshagent/openai/proxy/proxy.py +31 -0
- meshagent/openai/tools/__init__.py +2 -1
- meshagent/openai/tools/completions_adapter.py +9 -28
- meshagent/openai/tools/responses_adapter.py +874 -261
- meshagent/openai/tools/schema.py +202 -0
- meshagent/openai/tools/stt.py +88 -0
- meshagent/openai/tools/stt_test.py +85 -0
- meshagent/openai/version.py +1 -1
- {meshagent_openai-0.0.29.dist-info → meshagent_openai-0.0.31.dist-info}/METADATA +5 -5
- meshagent_openai-0.0.31.dist-info/RECORD +15 -0
- meshagent_openai-0.0.29.dist-info/RECORD +0 -10
- {meshagent_openai-0.0.29.dist-info → meshagent_openai-0.0.31.dist-info}/WHEEL +0 -0
- {meshagent_openai-0.0.29.dist-info → meshagent_openai-0.0.31.dist-info}/licenses/LICENSE +0 -0
- {meshagent_openai-0.0.29.dist-info → meshagent_openai-0.0.31.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
from openai import
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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"
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
+
span.set_attributes({
|
|
400
|
+
"type" : message.type,
|
|
401
|
+
"message" : message.model_dump_json()
|
|
402
|
+
})
|
|
433
403
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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("
|
|
470
|
-
error =
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
576
|
+
event : ResponseStreamEvent = e
|
|
577
|
+
span.set_attributes({
|
|
578
|
+
"type" : event.type,
|
|
579
|
+
"event" : event.model_dump_json()
|
|
580
|
+
})
|
|
499
581
|
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
603
|
+
return text
|
|
522
604
|
|
|
523
|
-
|
|
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
|
-
|
|
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
|
-
|
|
621
|
+
else:
|
|
622
|
+
for toolkit in toolkits:
|
|
623
|
+
for tool in toolkit.tools:
|
|
540
624
|
|
|
541
|
-
|
|
625
|
+
if isinstance(tool, OpenAIResponsesTool):
|
|
542
626
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|