mbxai 0.6.10__py3-none-any.whl → 0.6.12__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.
- mbxai/__init__.py +1 -1
- mbxai/mcp/server.py +1 -1
- mbxai/tools/client.py +167 -110
- {mbxai-0.6.10.dist-info → mbxai-0.6.12.dist-info}/METADATA +1 -1
- {mbxai-0.6.10.dist-info → mbxai-0.6.12.dist-info}/RECORD +7 -7
- {mbxai-0.6.10.dist-info → mbxai-0.6.12.dist-info}/WHEEL +0 -0
- {mbxai-0.6.10.dist-info → mbxai-0.6.12.dist-info}/licenses/LICENSE +0 -0
mbxai/__init__.py
CHANGED
mbxai/mcp/server.py
CHANGED
mbxai/tools/client.py
CHANGED
@@ -16,90 +16,40 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
T = TypeVar("T", bound=BaseModel)
|
17
17
|
|
18
18
|
class ToolClient:
|
19
|
-
"""
|
19
|
+
"""Client for handling tool calls with OpenRouter."""
|
20
20
|
|
21
|
-
def __init__(self, openrouter_client: OpenRouterClient):
|
22
|
-
"""Initialize the
|
23
|
-
self._openrouter_client = openrouter_client
|
24
|
-
self._tools: dict[str, Tool] = {}
|
21
|
+
def __init__(self, openrouter_client: OpenRouterClient) -> None:
|
22
|
+
"""Initialize the ToolClient.
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
Args:
|
25
|
+
openrouter_client: The OpenRouter client to use
|
26
|
+
"""
|
27
|
+
self._client = openrouter_client
|
28
|
+
self._tools: dict[str, Tool] = {}
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
return await tool.function(**kwargs)
|
30
|
+
def register_tool(
|
31
|
+
self,
|
32
|
+
name: str,
|
33
|
+
description: str,
|
34
|
+
function: Callable[..., Any],
|
35
|
+
schema: dict[str, Any],
|
36
|
+
) -> None:
|
37
|
+
"""Register a new tool.
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
39
|
+
Args:
|
40
|
+
name: The name of the tool
|
41
|
+
description: A description of what the tool does
|
42
|
+
function: The function to call when the tool is used
|
43
|
+
schema: The JSON schema for the tool's parameters
|
44
|
+
"""
|
45
|
+
tool = Tool(
|
46
|
+
name=name,
|
47
|
+
description=description,
|
48
|
+
function=function,
|
49
|
+
schema=schema,
|
51
50
|
)
|
52
|
-
|
53
|
-
|
54
|
-
if not response:
|
55
|
-
raise ValueError("No response received from OpenRouter")
|
56
|
-
|
57
|
-
if not response.choices:
|
58
|
-
raise ValueError("Response missing choices")
|
59
|
-
|
60
|
-
choice = response.choices[0]
|
61
|
-
if not choice:
|
62
|
-
raise ValueError("Empty choice in response")
|
63
|
-
|
64
|
-
message = choice.message
|
65
|
-
if not message:
|
66
|
-
raise ValueError("Choice missing message")
|
67
|
-
|
68
|
-
# If message has function call, execute it
|
69
|
-
if message.function_call:
|
70
|
-
tool_name = message.function_call.name
|
71
|
-
tool_args = json.loads(message.function_call.arguments)
|
72
|
-
|
73
|
-
# Invoke the tool
|
74
|
-
tool_response = await self.invoke_tool(tool_name, **tool_args)
|
75
|
-
|
76
|
-
# Add tool response to messages
|
77
|
-
messages.append({
|
78
|
-
"role": "assistant",
|
79
|
-
"content": None,
|
80
|
-
"function_call": {
|
81
|
-
"name": tool_name,
|
82
|
-
"arguments": message.function_call.arguments,
|
83
|
-
},
|
84
|
-
})
|
85
|
-
messages.append({
|
86
|
-
"role": "function",
|
87
|
-
"name": tool_name,
|
88
|
-
"content": json.dumps(tool_response),
|
89
|
-
})
|
90
|
-
|
91
|
-
# Get final response
|
92
|
-
final_response = await self._openrouter_client.chat_completion(
|
93
|
-
messages=messages,
|
94
|
-
model=model,
|
95
|
-
)
|
96
|
-
|
97
|
-
if not final_response or not final_response.choices:
|
98
|
-
raise ValueError("No response received after tool execution")
|
99
|
-
|
100
|
-
return final_response
|
101
|
-
|
102
|
-
return response
|
51
|
+
self._tools[name] = tool
|
52
|
+
logger.info(f"Registered tool: {name}")
|
103
53
|
|
104
54
|
def _truncate_content(self, content: str | None, max_length: int = 100) -> str:
|
105
55
|
"""Truncate content for logging."""
|
@@ -246,12 +196,123 @@ class ToolClient:
|
|
246
196
|
# Log the messages we're about to send
|
247
197
|
self._log_messages(messages, validate_responses=False)
|
248
198
|
|
199
|
+
async def chat(
|
200
|
+
self,
|
201
|
+
messages: list[dict[str, Any]],
|
202
|
+
*,
|
203
|
+
model: str | None = None,
|
204
|
+
stream: bool = False,
|
205
|
+
**kwargs: Any,
|
206
|
+
) -> Any:
|
207
|
+
"""Chat with the model, handling tool calls."""
|
208
|
+
tools = [tool.to_openai_function() for tool in self._tools.values()]
|
209
|
+
|
210
|
+
if tools:
|
211
|
+
logger.info(f"Available tools: {[tool['function']['name'] for tool in tools]}")
|
212
|
+
kwargs["tools"] = tools
|
213
|
+
kwargs["tool_choice"] = "auto"
|
214
|
+
|
215
|
+
while True:
|
216
|
+
# Get the model's response
|
217
|
+
response = self._client.chat_completion(
|
218
|
+
messages=messages,
|
219
|
+
model=model,
|
220
|
+
stream=stream,
|
221
|
+
**kwargs,
|
222
|
+
)
|
223
|
+
|
224
|
+
if stream:
|
225
|
+
return response
|
226
|
+
|
227
|
+
message = response.choices[0].message
|
228
|
+
# Add the assistant's message with tool calls
|
229
|
+
assistant_message = {
|
230
|
+
"role": "assistant",
|
231
|
+
"content": message.content or None, # Ensure content is None if empty
|
232
|
+
}
|
233
|
+
if message.tool_calls:
|
234
|
+
assistant_message["tool_calls"] = [
|
235
|
+
{
|
236
|
+
"id": tool_call.id,
|
237
|
+
"type": "function",
|
238
|
+
"function": {
|
239
|
+
"name": tool_call.function.name,
|
240
|
+
"arguments": tool_call.function.arguments,
|
241
|
+
},
|
242
|
+
}
|
243
|
+
for tool_call in message.tool_calls
|
244
|
+
]
|
245
|
+
messages.append(assistant_message)
|
246
|
+
logger.info(f"Message count: {len(messages)}, Added assistant message with tool calls: {[tc.function.name for tc in message.tool_calls] if message.tool_calls else None}")
|
247
|
+
|
248
|
+
# If there are no tool calls, we're done
|
249
|
+
if not message.tool_calls:
|
250
|
+
return response
|
251
|
+
|
252
|
+
# Process all tool calls
|
253
|
+
tool_responses = []
|
254
|
+
for tool_call in message.tool_calls:
|
255
|
+
tool = self._tools.get(tool_call.function.name)
|
256
|
+
if not tool:
|
257
|
+
raise ValueError(f"Unknown tool: {tool_call.function.name}")
|
258
|
+
|
259
|
+
# Parse arguments if they're a string
|
260
|
+
arguments = tool_call.function.arguments
|
261
|
+
if isinstance(arguments, str):
|
262
|
+
try:
|
263
|
+
arguments = json.loads(arguments)
|
264
|
+
except json.JSONDecodeError as e:
|
265
|
+
logger.error(f"Failed to parse tool arguments: {e}")
|
266
|
+
raise ValueError(f"Invalid tool arguments format: {arguments}")
|
267
|
+
|
268
|
+
# Call the tool
|
269
|
+
logger.info(f"Calling tool: {tool.name} with args: {self._truncate_dict(arguments)}")
|
270
|
+
try:
|
271
|
+
if inspect.iscoroutinefunction(tool.function):
|
272
|
+
result = await asyncio.wait_for(tool.function(**arguments), timeout=300.0) # 5 minutes timeout
|
273
|
+
else:
|
274
|
+
result = tool.function(**arguments)
|
275
|
+
logger.info(f"Tool {tool.name} completed successfully")
|
276
|
+
except asyncio.TimeoutError:
|
277
|
+
logger.error(f"Tool {tool.name} timed out after 5 minutes")
|
278
|
+
result = {"error": "Tool execution timed out after 5 minutes"}
|
279
|
+
except Exception as e:
|
280
|
+
logger.error(f"Error calling tool {tool.name}: {str(e)}")
|
281
|
+
result = {"error": f"Tool execution failed: {str(e)}"}
|
282
|
+
|
283
|
+
# Convert result to JSON string if it's not already
|
284
|
+
if not isinstance(result, str):
|
285
|
+
result = json.dumps(result)
|
286
|
+
|
287
|
+
# Create the tool response
|
288
|
+
tool_response = {
|
289
|
+
"role": "tool",
|
290
|
+
"tool_call_id": tool_call.id,
|
291
|
+
"content": result,
|
292
|
+
}
|
293
|
+
tool_responses.append(tool_response)
|
294
|
+
logger.info(f"Created tool response for call ID {tool_call.id}")
|
295
|
+
|
296
|
+
# Add all tool responses to the messages
|
297
|
+
messages.extend(tool_responses)
|
298
|
+
logger.info(f"Message count: {len(messages)}, Added {len(tool_responses)} tool responses to messages")
|
299
|
+
|
300
|
+
# Validate the message sequence
|
301
|
+
self._validate_message_sequence(messages, validate_responses=True)
|
302
|
+
|
303
|
+
# Log the messages we're about to send
|
304
|
+
self._log_messages(messages, validate_responses=False)
|
305
|
+
|
306
|
+
# Continue the loop to get the next response
|
307
|
+
continue
|
308
|
+
|
249
309
|
async def parse(
|
250
310
|
self,
|
251
|
-
messages: list[dict[str,
|
311
|
+
messages: list[dict[str, Any]],
|
252
312
|
response_format: type[T],
|
253
313
|
*,
|
254
314
|
model: str | None = None,
|
315
|
+
stream: bool = False,
|
255
316
|
**kwargs: Any,
|
256
317
|
) -> Any:
|
257
318
|
"""Chat with the model and parse the response into a Pydantic model.
|
@@ -260,46 +321,42 @@ class ToolClient:
|
|
260
321
|
messages: The conversation messages
|
261
322
|
response_format: The Pydantic model to parse the response into
|
262
323
|
model: Optional model override
|
324
|
+
stream: Whether to stream the response
|
263
325
|
**kwargs: Additional parameters for the chat completion
|
264
326
|
|
265
327
|
Returns:
|
266
328
|
The parsed response from the model
|
267
329
|
"""
|
268
|
-
# First use
|
269
|
-
|
330
|
+
# First, use chat to handle any tool calls
|
331
|
+
chat_response = await self.chat(
|
270
332
|
messages=messages,
|
271
333
|
model=model,
|
334
|
+
stream=stream,
|
272
335
|
**kwargs,
|
273
336
|
)
|
274
337
|
|
275
|
-
if
|
276
|
-
|
277
|
-
|
278
|
-
choice = response.choices[0]
|
279
|
-
if not choice:
|
280
|
-
raise ValueError("Empty choice in response")
|
281
|
-
|
282
|
-
message = choice.message
|
283
|
-
if not message:
|
284
|
-
raise ValueError("Choice missing message")
|
338
|
+
if stream:
|
339
|
+
return chat_response
|
285
340
|
|
286
|
-
#
|
287
|
-
|
288
|
-
raise ValueError("Unexpected tool calls in final response")
|
341
|
+
# Get the final message after all tool calls are handled
|
342
|
+
final_message = chat_response.choices[0].message
|
289
343
|
|
290
|
-
#
|
291
|
-
|
292
|
-
|
344
|
+
# Create a new message list with just the final response
|
345
|
+
parse_messages = [
|
346
|
+
*messages, # Include original context
|
347
|
+
{
|
348
|
+
"role": "assistant",
|
349
|
+
"content": final_message.content,
|
350
|
+
}
|
351
|
+
]
|
293
352
|
|
294
|
-
#
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
logger.error(f"Failed to parse response: {e}")
|
305
|
-
raise ValueError(f"Failed to parse response as {response_format.__name__}: {str(e)}")
|
353
|
+
# Make a final parse request with the structured output format
|
354
|
+
parse_kwargs = kwargs.copy()
|
355
|
+
parse_kwargs["response_format"] = response_format
|
356
|
+
|
357
|
+
return self._client.chat_completion_parse(
|
358
|
+
messages=parse_messages,
|
359
|
+
model=model,
|
360
|
+
stream=stream,
|
361
|
+
**parse_kwargs,
|
362
|
+
)
|
@@ -1,18 +1,18 @@
|
|
1
|
-
mbxai/__init__.py,sha256=
|
1
|
+
mbxai/__init__.py,sha256=TPQPmobkBuhgo14NoMCbpjcR4Lgi3T-hc27EY0cDggk,48
|
2
2
|
mbxai/core.py,sha256=WMvmU9TTa7M_m-qWsUew4xH8Ul6xseCZ2iBCXJTW-Bs,196
|
3
3
|
mbxai/mcp/__init__.py,sha256=_ek9iYdYqW5saKetj4qDci11jxesQDiHPJRpHMKkxgU,175
|
4
4
|
mbxai/mcp/client.py,sha256=B8ZpH-uecmTCgoDw65LwwVxsFWVoX-08t5ff0hOEPXk,6011
|
5
5
|
mbxai/mcp/example.py,sha256=oaol7AvvZnX86JWNz64KvPjab5gg1VjVN3G8eFSzuaE,2350
|
6
|
-
mbxai/mcp/server.py,sha256=
|
6
|
+
mbxai/mcp/server.py,sha256=T0-Y7FeHRFqSTp2ERU96fOQlQJKjMFxg8oqC4dzBmBA,3463
|
7
7
|
mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
|
8
8
|
mbxai/openrouter/client.py,sha256=RO5tbF42vkcjxjvC-QFB8DGA0gQLljH3KPBn3HgZV8I,13662
|
9
9
|
mbxai/openrouter/config.py,sha256=Ia93s-auim9Sq71eunVDbn9ET5xX2zusXpV4JBdHAzs,3251
|
10
10
|
mbxai/openrouter/models.py,sha256=b3IjjtZAjeGOf2rLsdnCD1HacjTnS8jmv_ZXorc-KJQ,2604
|
11
11
|
mbxai/tools/__init__.py,sha256=QUFaXhDm-UKcuAtT1rbKzhBkvyRBVokcQIOf9cxIuwc,160
|
12
|
-
mbxai/tools/client.py,sha256=
|
12
|
+
mbxai/tools/client.py,sha256=sK2sl8VKB7sgMHtJpnC_2gfy7HCA4RMzRb32sfXvWbI,14260
|
13
13
|
mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
|
14
14
|
mbxai/tools/types.py,sha256=fo5t9UbsHGynhA88vD_ecgDqL8iLvt2E1h1ym43Rrgk,745
|
15
|
-
mbxai-0.6.
|
16
|
-
mbxai-0.6.
|
17
|
-
mbxai-0.6.
|
18
|
-
mbxai-0.6.
|
15
|
+
mbxai-0.6.12.dist-info/METADATA,sha256=3Iwx5kOqV1v-wEQ6FskMYvz-j8AuiXRFHmpvGkUT8HA,4108
|
16
|
+
mbxai-0.6.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
mbxai-0.6.12.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
|
18
|
+
mbxai-0.6.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|