mbxai 0.5.19__tar.gz → 0.5.21__tar.gz
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-0.5.19 → mbxai-0.5.21}/PKG-INFO +1 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/pyproject.toml +1 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/setup.py +1 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/__init__.py +1 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/mcp/client.py +2 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/mcp/server.py +1 -1
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/tools/client.py +89 -80
- {mbxai-0.5.19 → mbxai-0.5.21}/uv.lock +7 -7
- {mbxai-0.5.19 → mbxai-0.5.21}/.vscode/PythonImportHelper-v2-Completion.json +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/LICENSE +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/README.md +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/core.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/mcp/__init__.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/mcp/example.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/openrouter/__init__.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/openrouter/client.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/openrouter/config.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/openrouter/models.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/tools/__init__.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/tools/example.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/src/mbxai/tools/types.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/tests/test_core.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/tests/test_mcp.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/tests/test_openrouter.py +0 -0
- {mbxai-0.5.19 → mbxai-0.5.21}/tests/test_tools.py +0 -0
@@ -90,7 +90,8 @@ class MCPClient(ToolClient):
|
|
90
90
|
# Make the HTTP request to the tool's URL
|
91
91
|
response = await self._http_client.post(
|
92
92
|
url,
|
93
|
-
json={"input": kwargs} if tool.strict else kwargs
|
93
|
+
json={"input": kwargs} if tool.strict else kwargs,
|
94
|
+
timeout=300.0 # 5 minutes timeout
|
94
95
|
)
|
95
96
|
return response.json()
|
96
97
|
|
@@ -9,6 +9,7 @@ import json
|
|
9
9
|
from pydantic import BaseModel
|
10
10
|
from ..openrouter import OpenRouterClient
|
11
11
|
from .types import Tool, ToolCall
|
12
|
+
import asyncio
|
12
13
|
|
13
14
|
logger = logging.getLogger(__name__)
|
14
15
|
|
@@ -139,6 +140,62 @@ class ToolClient:
|
|
139
140
|
# Validate message sequence
|
140
141
|
self._validate_message_sequence(messages, validate_responses)
|
141
142
|
|
143
|
+
async def _process_tool_calls(self, message: Any, messages: list[dict[str, Any]]) -> None:
|
144
|
+
"""Process all tool calls in a message.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
message: The message containing tool calls
|
148
|
+
messages: The list of messages to add responses to
|
149
|
+
"""
|
150
|
+
if not message.tool_calls:
|
151
|
+
return
|
152
|
+
|
153
|
+
# Process all tool calls first
|
154
|
+
tool_responses = []
|
155
|
+
for tool_call in message.tool_calls:
|
156
|
+
tool = self._tools.get(tool_call.function.name)
|
157
|
+
if not tool:
|
158
|
+
raise ValueError(f"Unknown tool: {tool_call.function.name}")
|
159
|
+
|
160
|
+
# Parse arguments if they're a string
|
161
|
+
arguments = tool_call.function.arguments
|
162
|
+
if isinstance(arguments, str):
|
163
|
+
try:
|
164
|
+
arguments = json.loads(arguments)
|
165
|
+
except json.JSONDecodeError as e:
|
166
|
+
logger.error(f"Failed to parse tool arguments: {e}")
|
167
|
+
raise ValueError(f"Invalid tool arguments format: {arguments}")
|
168
|
+
|
169
|
+
# Call the tool
|
170
|
+
logger.info(f"Calling tool: {tool.name} with args: {self._truncate_dict(arguments)}")
|
171
|
+
if inspect.iscoroutinefunction(tool.function):
|
172
|
+
result = await tool.function(**arguments)
|
173
|
+
else:
|
174
|
+
result = tool.function(**arguments)
|
175
|
+
|
176
|
+
# Convert result to JSON string if it's not already
|
177
|
+
if not isinstance(result, str):
|
178
|
+
result = json.dumps(result)
|
179
|
+
|
180
|
+
# Create the tool response
|
181
|
+
tool_response = {
|
182
|
+
"role": "tool",
|
183
|
+
"tool_call_id": tool_call.id,
|
184
|
+
"content": result,
|
185
|
+
}
|
186
|
+
tool_responses.append(tool_response)
|
187
|
+
logger.info(f"Created tool response for call ID {tool_call.id}")
|
188
|
+
|
189
|
+
# Add all tool responses to the messages
|
190
|
+
messages.extend(tool_responses)
|
191
|
+
logger.info(f"Message count: {len(messages)}, Added {len(tool_responses)} tool responses to messages")
|
192
|
+
|
193
|
+
# Validate the message sequence
|
194
|
+
self._validate_message_sequence(messages, validate_responses=True)
|
195
|
+
|
196
|
+
# Log the messages we're about to send
|
197
|
+
self._log_messages(messages, validate_responses=False)
|
198
|
+
|
142
199
|
async def chat(
|
143
200
|
self,
|
144
201
|
messages: list[dict[str, Any]],
|
@@ -186,13 +243,13 @@ class ToolClient:
|
|
186
243
|
for tool_call in message.tool_calls
|
187
244
|
]
|
188
245
|
messages.append(assistant_message)
|
189
|
-
logger.info(f"Added assistant message with tool calls: {[tc.function.name for tc in message.tool_calls] if message.tool_calls else None}")
|
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}")
|
190
247
|
|
191
248
|
# If there are no tool calls, we're done
|
192
249
|
if not message.tool_calls:
|
193
250
|
return response
|
194
251
|
|
195
|
-
# Process all tool calls
|
252
|
+
# Process all tool calls
|
196
253
|
tool_responses = []
|
197
254
|
for tool_call in message.tool_calls:
|
198
255
|
tool = self._tools.get(tool_call.function.name)
|
@@ -210,10 +267,18 @@ class ToolClient:
|
|
210
267
|
|
211
268
|
# Call the tool
|
212
269
|
logger.info(f"Calling tool: {tool.name} with args: {self._truncate_dict(arguments)}")
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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)}"}
|
217
282
|
|
218
283
|
# Convert result to JSON string if it's not already
|
219
284
|
if not isinstance(result, str):
|
@@ -230,7 +295,7 @@ class ToolClient:
|
|
230
295
|
|
231
296
|
# Add all tool responses to the messages
|
232
297
|
messages.extend(tool_responses)
|
233
|
-
logger.info(f"Added {len(tool_responses)} tool responses to messages")
|
298
|
+
logger.info(f"Message count: {len(messages)}, Added {len(tool_responses)} tool responses to messages")
|
234
299
|
|
235
300
|
# Validate the message sequence
|
236
301
|
self._validate_message_sequence(messages, validate_responses=True)
|
@@ -238,41 +303,8 @@ class ToolClient:
|
|
238
303
|
# Log the messages we're about to send
|
239
304
|
self._log_messages(messages, validate_responses=False)
|
240
305
|
|
241
|
-
#
|
242
|
-
|
243
|
-
messages=messages,
|
244
|
-
model=model,
|
245
|
-
stream=stream,
|
246
|
-
**kwargs,
|
247
|
-
)
|
248
|
-
|
249
|
-
if stream:
|
250
|
-
return response
|
251
|
-
|
252
|
-
message = response.choices[0].message
|
253
|
-
# Add the assistant's message with tool calls
|
254
|
-
assistant_message = {
|
255
|
-
"role": "assistant",
|
256
|
-
"content": message.content or None, # Ensure content is None if empty
|
257
|
-
}
|
258
|
-
if message.tool_calls:
|
259
|
-
assistant_message["tool_calls"] = [
|
260
|
-
{
|
261
|
-
"id": tool_call.id,
|
262
|
-
"type": "function",
|
263
|
-
"function": {
|
264
|
-
"name": tool_call.function.name,
|
265
|
-
"arguments": tool_call.function.arguments,
|
266
|
-
},
|
267
|
-
}
|
268
|
-
for tool_call in message.tool_calls
|
269
|
-
]
|
270
|
-
messages.append(assistant_message)
|
271
|
-
logger.info(f"Added assistant message with tool calls: {[tc.function.name for tc in message.tool_calls] if message.tool_calls else None}")
|
272
|
-
|
273
|
-
# If there are no more tool calls, we're done
|
274
|
-
if not message.tool_calls:
|
275
|
-
return response
|
306
|
+
# Continue the loop to get the next response
|
307
|
+
continue
|
276
308
|
|
277
309
|
async def parse(
|
278
310
|
self,
|
@@ -337,13 +369,14 @@ class ToolClient:
|
|
337
369
|
for tool_call in message.tool_calls
|
338
370
|
]
|
339
371
|
messages.append(assistant_message)
|
340
|
-
logger.info(f"
|
372
|
+
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}")
|
341
373
|
|
342
374
|
# If there are no tool calls, we're done
|
343
375
|
if not message.tool_calls:
|
344
376
|
return response
|
345
377
|
|
346
|
-
#
|
378
|
+
# Process all tool calls
|
379
|
+
tool_responses = []
|
347
380
|
for tool_call in message.tool_calls:
|
348
381
|
tool = self._tools.get(tool_call.function.name)
|
349
382
|
if not tool:
|
@@ -369,48 +402,24 @@ class ToolClient:
|
|
369
402
|
if not isinstance(result, str):
|
370
403
|
result = json.dumps(result)
|
371
404
|
|
372
|
-
# Create
|
405
|
+
# Create the tool response
|
373
406
|
tool_response = {
|
374
407
|
"role": "tool",
|
375
408
|
"tool_call_id": tool_call.id,
|
376
409
|
"content": result,
|
377
410
|
}
|
378
|
-
|
379
|
-
logger.info(f"
|
411
|
+
tool_responses.append(tool_response)
|
412
|
+
logger.info(f"Created tool response for call ID {tool_call.id}")
|
380
413
|
|
381
|
-
#
|
382
|
-
|
383
|
-
|
384
|
-
response_format=response_format,
|
385
|
-
model=model,
|
386
|
-
stream=stream,
|
387
|
-
**kwargs,
|
388
|
-
)
|
414
|
+
# Add all tool responses to the messages
|
415
|
+
messages.extend(tool_responses)
|
416
|
+
logger.info(f"Message count: {len(messages)}, Added {len(tool_responses)} tool responses to messages")
|
389
417
|
|
390
|
-
|
391
|
-
|
418
|
+
# Validate the message sequence
|
419
|
+
self._validate_message_sequence(messages, validate_responses=True)
|
392
420
|
|
393
|
-
|
394
|
-
|
395
|
-
assistant_message = {
|
396
|
-
"role": "assistant",
|
397
|
-
"content": message.content or None, # Ensure content is None if empty
|
398
|
-
}
|
399
|
-
if message.tool_calls:
|
400
|
-
assistant_message["tool_calls"] = [
|
401
|
-
{
|
402
|
-
"id": tool_call.id,
|
403
|
-
"type": "function",
|
404
|
-
"function": {
|
405
|
-
"name": tool_call.function.name,
|
406
|
-
"arguments": tool_call.function.arguments,
|
407
|
-
},
|
408
|
-
}
|
409
|
-
for tool_call in message.tool_calls
|
410
|
-
]
|
411
|
-
messages.append(assistant_message)
|
412
|
-
logger.info(f"Assistant message: content='{self._truncate_content(message.content)}', tool_calls={[tc.function.name for tc in message.tool_calls] if message.tool_calls else None}")
|
421
|
+
# Log the messages we're about to send
|
422
|
+
self._log_messages(messages, validate_responses=False)
|
413
423
|
|
414
|
-
#
|
415
|
-
|
416
|
-
return response
|
424
|
+
# Continue the loop to get the next response
|
425
|
+
continue
|
@@ -292,11 +292,11 @@ wheels = [
|
|
292
292
|
|
293
293
|
[[package]]
|
294
294
|
name = "httpx-sse"
|
295
|
-
version = "0.5.
|
295
|
+
version = "0.5.21"
|
296
296
|
source = { registry = "https://pypi.org/simple" }
|
297
|
-
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.5.
|
297
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.5.21.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
298
298
|
wheels = [
|
299
|
-
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.5.
|
299
|
+
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.5.21-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
300
300
|
]
|
301
301
|
|
302
302
|
[[package]]
|
@@ -446,7 +446,7 @@ wheels = [
|
|
446
446
|
|
447
447
|
[[package]]
|
448
448
|
name = "mbxai"
|
449
|
-
version = "0.5.
|
449
|
+
version = "0.5.21"
|
450
450
|
source = { editable = "." }
|
451
451
|
dependencies = [
|
452
452
|
{ name = "fastapi" },
|
@@ -980,14 +980,14 @@ wheels = [
|
|
980
980
|
|
981
981
|
[[package]]
|
982
982
|
name = "typing-inspection"
|
983
|
-
version = "0.5.
|
983
|
+
version = "0.5.21"
|
984
984
|
source = { registry = "https://pypi.org/simple" }
|
985
985
|
dependencies = [
|
986
986
|
{ name = "typing-extensions" },
|
987
987
|
]
|
988
|
-
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.5.
|
988
|
+
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.5.21.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
|
989
989
|
wheels = [
|
990
|
-
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.5.
|
990
|
+
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.5.21-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
991
991
|
]
|
992
992
|
|
993
993
|
[[package]]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|