mbxai 0.5.25__py3-none-any.whl → 0.6.0__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 +80 -140
- {mbxai-0.5.25.dist-info → mbxai-0.6.0.dist-info}/METADATA +1 -1
- {mbxai-0.5.25.dist-info → mbxai-0.6.0.dist-info}/RECORD +7 -7
- {mbxai-0.5.25.dist-info → mbxai-0.6.0.dist-info}/WHEEL +0 -0
- {mbxai-0.5.25.dist-info → mbxai-0.6.0.dist-info}/licenses/LICENSE +0 -0
mbxai/__init__.py
CHANGED
mbxai/mcp/server.py
CHANGED
mbxai/tools/client.py
CHANGED
@@ -16,40 +16,90 @@ logger = logging.getLogger(__name__)
|
|
16
16
|
T = TypeVar("T", bound=BaseModel)
|
17
17
|
|
18
18
|
class ToolClient:
|
19
|
-
"""
|
19
|
+
"""Base class for tool clients."""
|
20
20
|
|
21
|
-
def __init__(self, openrouter_client: OpenRouterClient)
|
22
|
-
"""Initialize the
|
23
|
-
|
24
|
-
Args:
|
25
|
-
openrouter_client: The OpenRouter client to use
|
26
|
-
"""
|
27
|
-
self._client = openrouter_client
|
21
|
+
def __init__(self, openrouter_client: OpenRouterClient):
|
22
|
+
"""Initialize the tool client."""
|
23
|
+
self._openrouter_client = openrouter_client
|
28
24
|
self._tools: dict[str, Tool] = {}
|
29
25
|
|
30
|
-
def register_tool(
|
31
|
-
|
32
|
-
name
|
33
|
-
description: str,
|
34
|
-
function: Callable[..., Any],
|
35
|
-
schema: dict[str, Any],
|
36
|
-
) -> None:
|
37
|
-
"""Register a new tool.
|
26
|
+
def register_tool(self, tool: Tool) -> None:
|
27
|
+
"""Register a tool."""
|
28
|
+
self._tools[tool.name] = tool
|
38
29
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
30
|
+
async def invoke_tool(self, tool_name: str, **kwargs: Any) -> Any:
|
31
|
+
"""Invoke a tool by name."""
|
32
|
+
tool = self._tools.get(tool_name)
|
33
|
+
if not tool:
|
34
|
+
raise ValueError(f"Tool {tool_name} not found")
|
35
|
+
|
36
|
+
if not tool.function:
|
37
|
+
raise ValueError(f"Tool {tool_name} has no function implementation")
|
38
|
+
|
39
|
+
return await tool.function(**kwargs)
|
40
|
+
|
41
|
+
async def chat(self, messages: list[dict[str, str]], model: str) -> Any:
|
42
|
+
"""Process a chat request with tools."""
|
43
|
+
# Convert tools to OpenAI function format
|
44
|
+
functions = [tool.to_openai_function() for tool in self._tools.values()]
|
45
|
+
|
46
|
+
# Make the chat request
|
47
|
+
response = await self._openrouter_client.chat(
|
48
|
+
messages=messages,
|
49
|
+
model=model,
|
50
|
+
functions=functions,
|
50
51
|
)
|
51
|
-
|
52
|
-
|
52
|
+
|
53
|
+
# Validate response
|
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(
|
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
|
53
103
|
|
54
104
|
def _truncate_content(self, content: str | None, max_length: int = 100) -> str:
|
55
105
|
"""Truncate content for logging."""
|
@@ -196,116 +246,6 @@ class ToolClient:
|
|
196
246
|
# Log the messages we're about to send
|
197
247
|
self._log_messages(messages, validate_responses=False)
|
198
248
|
|
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
|
-
|
309
249
|
async def parse(
|
310
250
|
self,
|
311
251
|
messages: list[dict[str, Any]],
|
@@ -339,7 +279,7 @@ class ToolClient:
|
|
339
279
|
self._log_messages(messages)
|
340
280
|
|
341
281
|
# Get the model's response
|
342
|
-
response = self.
|
282
|
+
response = self._openrouter_client.chat_completion_parse(
|
343
283
|
messages=messages,
|
344
284
|
response_format=response_format,
|
345
285
|
model=model,
|
@@ -1,18 +1,18 @@
|
|
1
|
-
mbxai/__init__.py,sha256=
|
1
|
+
mbxai/__init__.py,sha256=Jcym8UkXy8bN6bXrlc6D6eL9sjPD8EC0xMMW7BJ3Zt0,47
|
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=WUBsnIuSc9kKpZRm_JVnML0PYQOjRqon3RIv1qlJuHw,3462
|
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=ozIuWTQV4jePnKJrBHc8ZxMPUWfTbCqc1pOoImWoHrk,14577
|
13
13
|
mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
|
14
14
|
mbxai/tools/types.py,sha256=fo5t9UbsHGynhA88vD_ecgDqL8iLvt2E1h1ym43Rrgk,745
|
15
|
-
mbxai-0.
|
16
|
-
mbxai-0.
|
17
|
-
mbxai-0.
|
18
|
-
mbxai-0.
|
15
|
+
mbxai-0.6.0.dist-info/METADATA,sha256=maISXH6b_8zmxBmzj-olxxDaM7vc9TbRmVlXdqVhbys,4107
|
16
|
+
mbxai-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
mbxai-0.6.0.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
|
18
|
+
mbxai-0.6.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|