mbxai 0.5.24__tar.gz → 0.6.0__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.
Files changed (26) hide show
  1. mbxai-0.6.0/.gitignore +94 -0
  2. {mbxai-0.5.24 → mbxai-0.6.0}/PKG-INFO +1 -1
  3. {mbxai-0.5.24 → mbxai-0.6.0}/pyproject.toml +1 -1
  4. {mbxai-0.5.24 → mbxai-0.6.0}/setup.py +1 -1
  5. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/__init__.py +1 -1
  6. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/mcp/server.py +1 -1
  7. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/openrouter/client.py +1 -0
  8. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/tools/client.py +80 -140
  9. {mbxai-0.5.24 → mbxai-0.6.0}/uv.lock +7 -7
  10. mbxai-0.5.24/.vscode/PythonImportHelper-v2-Completion.json +0 -1442
  11. {mbxai-0.5.24 → mbxai-0.6.0}/LICENSE +0 -0
  12. {mbxai-0.5.24 → mbxai-0.6.0}/README.md +0 -0
  13. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/core.py +0 -0
  14. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/mcp/__init__.py +0 -0
  15. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/mcp/client.py +0 -0
  16. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/mcp/example.py +0 -0
  17. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/openrouter/__init__.py +0 -0
  18. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/openrouter/config.py +0 -0
  19. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/openrouter/models.py +0 -0
  20. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/tools/__init__.py +0 -0
  21. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/tools/example.py +0 -0
  22. {mbxai-0.5.24 → mbxai-0.6.0}/src/mbxai/tools/types.py +0 -0
  23. {mbxai-0.5.24 → mbxai-0.6.0}/tests/test_core.py +0 -0
  24. {mbxai-0.5.24 → mbxai-0.6.0}/tests/test_mcp.py +0 -0
  25. {mbxai-0.5.24 → mbxai-0.6.0}/tests/test_openrouter.py +0 -0
  26. {mbxai-0.5.24 → mbxai-0.6.0}/tests/test_tools.py +0 -0
mbxai-0.6.0/.gitignore ADDED
@@ -0,0 +1,94 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual Environment
25
+ .env
26
+ .venv
27
+ env/
28
+ venv/
29
+ ENV/
30
+ env.bak/
31
+ venv.bak/
32
+
33
+ # IDE
34
+ .idea/
35
+ .vscode/
36
+ *.swp
37
+ *.swo
38
+ .DS_Store
39
+
40
+ # Logs
41
+ *.log
42
+ logs/
43
+ log/
44
+
45
+ # Local development
46
+ .env.local
47
+ .env.development.local
48
+ .env.test.local
49
+ .env.production.local
50
+
51
+ # Coverage reports
52
+ htmlcov/
53
+ .tox/
54
+ .coverage
55
+ .coverage.*
56
+ .cache
57
+ nosetests.xml
58
+ coverage.xml
59
+ *.cover
60
+ .hypothesis/
61
+
62
+ # Jupyter Notebook
63
+ .ipynb_checkpoints
64
+
65
+ # mypy
66
+ .mypy_cache/
67
+ .dmypy.json
68
+ dmypy.json
69
+
70
+ # pytest
71
+ .pytest_cache/
72
+
73
+ # Distribution / packaging
74
+ .Python
75
+ build/
76
+ develop-eggs/
77
+ dist/
78
+ downloads/
79
+ eggs/
80
+ .eggs/
81
+ lib/
82
+ lib64/
83
+ parts/
84
+ sdist/
85
+ var/
86
+ wheels/
87
+ *.egg-info/
88
+ .installed.cfg
89
+ *.egg
90
+
91
+ # Project specific
92
+ *.db
93
+ *.sqlite3
94
+ node_modules/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 0.5.24
3
+ Version: 0.6.0
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mbxai"
7
- version = "0.5.24"
7
+ version = "0.6.0"
8
8
  authors = [
9
9
  { name = "MBX AI" }
10
10
  ]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mbxai",
5
- version="0.5.24",
5
+ version="0.6.0",
6
6
  author="MBX AI",
7
7
  description="MBX AI SDK",
8
8
  long_description=open("README.md").read(),
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "0.5.24"
5
+ __version__ = "0.6.0"
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="0.5.24",
34
+ version="0.6.0",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -9,6 +9,7 @@ from .models import OpenRouterModel, OpenRouterModelRegistry
9
9
  from .config import OpenRouterConfig
10
10
  import logging
11
11
  import time
12
+ import asyncio
12
13
  from functools import wraps
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -16,40 +16,90 @@ logger = logging.getLogger(__name__)
16
16
  T = TypeVar("T", bound=BaseModel)
17
17
 
18
18
  class ToolClient:
19
- """Client for handling tool calls with OpenRouter."""
19
+ """Base class for tool clients."""
20
20
 
21
- def __init__(self, openrouter_client: OpenRouterClient) -> None:
22
- """Initialize the ToolClient.
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
- self,
32
- name: str,
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
- 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,
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
- self._tools[name] = tool
52
- logger.info(f"Registered tool: {name}")
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._client.chat_completion_parse(
282
+ response = self._openrouter_client.chat_completion_parse(
343
283
  messages=messages,
344
284
  response_format=response_format,
345
285
  model=model,
@@ -292,11 +292,11 @@ wheels = [
292
292
 
293
293
  [[package]]
294
294
  name = "httpx-sse"
295
- version = "0.5.24"
295
+ version = "0.6.0"
296
296
  source = { registry = "https://pypi.org/simple" }
297
- sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.5.24.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
297
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.6.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
298
298
  wheels = [
299
- { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.5.24-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
299
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.6.0-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.24"
449
+ version = "0.6.0"
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.24"
983
+ version = "0.6.0"
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.24.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
988
+ sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.6.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
989
989
  wheels = [
990
- { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.5.24-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
990
+ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.6.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
991
991
  ]
992
992
 
993
993
  [[package]]