mbxai 0.6.17__tar.gz → 0.6.19__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.17 → mbxai-0.6.19}/PKG-INFO +2 -1
  2. {mbxai-0.6.17 → mbxai-0.6.19}/pyproject.toml +3 -2
  3. {mbxai-0.6.17 → mbxai-0.6.19}/setup.py +1 -1
  4. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/__init__.py +1 -1
  5. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/mcp/client.py +60 -43
  6. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/mcp/server.py +1 -1
  7. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/tools/__init__.py +2 -1
  8. mbxai-0.6.19/src/mbxai/tools/types.py +106 -0
  9. {mbxai-0.6.17 → mbxai-0.6.19}/uv.lock +9 -7
  10. mbxai-0.6.17/src/mbxai/tools/types.py +0 -95
  11. {mbxai-0.6.17 → mbxai-0.6.19}/.gitignore +0 -0
  12. {mbxai-0.6.17 → mbxai-0.6.19}/LICENSE +0 -0
  13. {mbxai-0.6.17 → mbxai-0.6.19}/README.md +0 -0
  14. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/core.py +0 -0
  15. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/mcp/__init__.py +0 -0
  16. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/mcp/example.py +0 -0
  17. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/openrouter/__init__.py +0 -0
  18. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/openrouter/client.py +0 -0
  19. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/openrouter/config.py +0 -0
  20. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/openrouter/models.py +0 -0
  21. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/tools/client.py +0 -0
  22. {mbxai-0.6.17 → mbxai-0.6.19}/src/mbxai/tools/example.py +0 -0
  23. {mbxai-0.6.17 → mbxai-0.6.19}/tests/test_core.py +0 -0
  24. {mbxai-0.6.17 → mbxai-0.6.19}/tests/test_mcp.py +0 -0
  25. {mbxai-0.6.17 → mbxai-0.6.19}/tests/test_openrouter.py +0 -0
  26. {mbxai-0.6.17 → mbxai-0.6.19}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 0.6.17
3
+ Version: 0.6.19
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -22,6 +22,7 @@ Requires-Dist: pydantic>=2.9.1
22
22
  Requires-Dist: python-multipart>=0.0.20
23
23
  Requires-Dist: sse-starlette>=2.3.4
24
24
  Requires-Dist: starlette>=0.46.2
25
+ Requires-Dist: typing-inspection<=0.4.0
25
26
  Requires-Dist: uvicorn>=0.34.2
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: black>=24.3.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mbxai"
7
- version = "0.6.17"
7
+ version = "0.6.19"
8
8
  authors = [
9
9
  { name = "MBX AI" }
10
10
  ]
@@ -29,7 +29,8 @@ dependencies = [
29
29
  "sse-starlette>=2.3.4",
30
30
  "starlette>=0.46.2",
31
31
  "uvicorn>=0.34.2",
32
- "pydantic-settings>=2.9.1"
32
+ "pydantic-settings>=2.9.1",
33
+ "typing-inspection<=0.4.0"
33
34
  ]
34
35
 
35
36
  [project.urls]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mbxai",
5
- version="0.6.17",
5
+ version="0.6.19",
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.25"
5
+ __version__ = "0.6.19"
@@ -4,9 +4,10 @@ from typing import Any, TypeVar, Callable
4
4
  import httpx
5
5
  import logging
6
6
  import asyncio
7
+ import json
7
8
  from pydantic import BaseModel, Field
8
9
 
9
- from ..tools import ToolClient, Tool
10
+ from ..tools import ToolClient, Tool, convert_to_strict_schema
10
11
  from ..openrouter import OpenRouterClient
11
12
 
12
13
  logger = logging.getLogger(__name__)
@@ -21,18 +22,22 @@ class MCPTool(Tool):
21
22
  strict: bool = Field(default=True, description="Whether the tool response is strictly validated")
22
23
  input_schema: dict[str, Any] = Field(description="The input schema for the tool")
23
24
  function: Callable[..., Any] | None = Field(default=None, description="The function that implements the tool")
25
+ _converted_schema: dict[str, Any] | None = None
24
26
 
25
27
  def to_openai_function(self) -> dict[str, Any]:
26
28
  """Convert the tool to an OpenAI function definition."""
27
- # Use the base Tool's schema conversion to ensure strict compliance
28
- strict_schema = self._ensure_strict_schema(self.input_schema)
29
+ # Use cached schema if available
30
+ if self._converted_schema is None:
31
+ # Convert the schema to strict format
32
+ self._converted_schema = convert_to_strict_schema(self.input_schema, self.strict)
33
+ logger.debug(f"Converted schema for {self.name}: {json.dumps(self._converted_schema, indent=2)}")
29
34
 
30
35
  return {
31
36
  "type": "function",
32
37
  "function": {
33
38
  "name": self.name,
34
39
  "description": self.description,
35
- "parameters": strict_schema
40
+ "parameters": self._converted_schema
36
41
  }
37
42
  }
38
43
 
@@ -41,19 +46,6 @@ class MCPTool(Tool):
41
46
  if not mcp_schema:
42
47
  return {"type": "object", "properties": {}, "required": []}
43
48
 
44
- # If schema has a $ref, resolve it
45
- if "$ref" in mcp_schema:
46
- ref = mcp_schema["$ref"].split("/")[-1]
47
- mcp_schema = mcp_schema.get("$defs", {}).get(ref, {})
48
-
49
- # If schema has an input wrapper, unwrap it
50
- if "properties" in mcp_schema and "input" in mcp_schema["properties"]:
51
- input_schema = mcp_schema["properties"]["input"]
52
- if "$ref" in input_schema:
53
- ref = input_schema["$ref"].split("/")[-1]
54
- input_schema = mcp_schema.get("$defs", {}).get(ref, {})
55
- mcp_schema = input_schema
56
-
57
49
  # Create a new schema object to ensure we have all required fields
58
50
  strict_schema = {
59
51
  "type": "object",
@@ -61,31 +53,56 @@ class MCPTool(Tool):
61
53
  "required": []
62
54
  }
63
55
 
64
- # Copy over properties, ensuring each has type and description
65
- if "properties" in mcp_schema:
66
- for prop_name, prop in mcp_schema["properties"].items():
67
- # Create a new property object with required fields
68
- new_prop = {
69
- "type": prop.get("type", "string"),
70
- "description": prop.get("description", f"The {prop_name} parameter")
71
- }
72
-
73
- # Copy over any additional fields that might be useful
74
- for key, value in prop.items():
75
- if key not in new_prop:
76
- new_prop[key] = value
77
-
78
- strict_schema["properties"][prop_name] = new_prop
79
-
80
- # Copy over required fields
81
- if "required" in mcp_schema:
82
- strict_schema["required"] = mcp_schema["required"]
56
+ # Add additionalProperties: false for strict tools
57
+ if self.strict:
58
+ strict_schema["additionalProperties"] = False
83
59
 
84
- # Ensure all required fields are actually in properties
85
- strict_schema["required"] = [
86
- req for req in strict_schema["required"]
87
- if req in strict_schema["properties"]
88
- ]
60
+ # Handle input wrapper
61
+ if "properties" in mcp_schema and "input" in mcp_schema["properties"]:
62
+ input_schema = mcp_schema["properties"]["input"]
63
+
64
+ # If input has a $ref, resolve it
65
+ if "$ref" in input_schema:
66
+ ref = input_schema["$ref"].split("/")[-1]
67
+ input_schema = mcp_schema.get("$defs", {}).get(ref, {})
68
+
69
+ # Create the input property schema
70
+ input_prop_schema = {
71
+ "type": "object",
72
+ "properties": {},
73
+ "required": []
74
+ }
75
+
76
+ # Add additionalProperties: false for input schema
77
+ if self.strict:
78
+ input_prop_schema["additionalProperties"] = False
79
+
80
+ # Copy over input properties
81
+ if "properties" in input_schema:
82
+ for prop_name, prop in input_schema["properties"].items():
83
+ # Create a new property object with required fields
84
+ new_prop = {
85
+ "type": prop.get("type", "string"),
86
+ "description": prop.get("description", f"The {prop_name} parameter")
87
+ }
88
+
89
+ # Copy over any additional fields that might be useful
90
+ for key, value in prop.items():
91
+ if key not in new_prop:
92
+ new_prop[key] = value
93
+
94
+ input_prop_schema["properties"][prop_name] = new_prop
95
+
96
+ # Copy over required fields for input schema
97
+ if "required" in input_schema:
98
+ input_prop_schema["required"] = input_schema["required"]
99
+
100
+ # Add the input property to the main schema
101
+ strict_schema["properties"]["input"] = input_prop_schema
102
+
103
+ # Copy over required fields for main schema
104
+ if "required" in mcp_schema:
105
+ strict_schema["required"] = mcp_schema["required"]
89
106
 
90
107
  return strict_schema
91
108
 
@@ -159,7 +176,7 @@ class MCPClient(ToolClient):
159
176
 
160
177
  # Register each tool
161
178
  for idx, tool_data in enumerate(tools_data):
162
- logger.debug(f"Processing tool {idx}: {tool_data}")
179
+ logger.debug(f"Processing tool {idx}: {json.dumps(tool_data, indent=2)}")
163
180
 
164
181
  # Ensure tool_data is a dictionary
165
182
  if not isinstance(tool_data, dict):
@@ -181,4 +198,4 @@ class MCPClient(ToolClient):
181
198
  logger.info(f"Successfully registered tool: {tool.name}")
182
199
  except Exception as e:
183
200
  logger.error(f"Failed to register tool: {str(e)}")
184
- logger.error(f"Tool data that caused the error: {tool_data}")
201
+ logger.error(f"Tool data that caused the error: {json.dumps(tool_data, indent=2)}")
@@ -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.25",
34
+ version="0.6.19",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -3,10 +3,11 @@ Tools module for MBX AI.
3
3
  """
4
4
 
5
5
  from .client import ToolClient
6
- from .types import Tool, ToolCall
6
+ from .types import Tool, ToolCall, convert_to_strict_schema
7
7
 
8
8
  __all__ = [
9
9
  "ToolClient",
10
10
  "Tool",
11
11
  "ToolCall",
12
+ "convert_to_strict_schema",
12
13
  ]
@@ -0,0 +1,106 @@
1
+ """
2
+ Type definitions for the tools package.
3
+ """
4
+
5
+ from typing import Any, Callable
6
+ from pydantic import BaseModel, Field
7
+
8
+ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True) -> dict[str, Any]:
9
+ """Convert a schema to strict format required by OpenAI.
10
+
11
+ Args:
12
+ schema: The input schema to validate and convert
13
+ strict: Whether to enforce strict validation with additionalProperties: false
14
+
15
+ Returns:
16
+ A schema in strict format
17
+ """
18
+ if not schema:
19
+ return {"type": "object", "properties": {}, "required": []}
20
+
21
+ # Create a new schema object to ensure we have all required fields
22
+ strict_schema = {
23
+ "type": "object",
24
+ "properties": {},
25
+ "required": []
26
+ }
27
+
28
+ # Add additionalProperties: false for strict validation
29
+ if strict:
30
+ strict_schema["additionalProperties"] = False
31
+
32
+ # Handle input wrapper
33
+ if "properties" in schema and "input" in schema["properties"]:
34
+ input_schema = schema["properties"]["input"]
35
+
36
+ # If input has a $ref, resolve it
37
+ if "$ref" in input_schema:
38
+ ref = input_schema["$ref"].split("/")[-1]
39
+ input_schema = schema.get("$defs", {}).get(ref, {})
40
+
41
+ # Create the input property schema
42
+ input_prop_schema = {
43
+ "type": "object",
44
+ "properties": {},
45
+ "required": []
46
+ }
47
+
48
+ # Add additionalProperties: false for input schema
49
+ if strict:
50
+ input_prop_schema["additionalProperties"] = False
51
+
52
+ # Copy over input properties
53
+ if "properties" in input_schema:
54
+ for prop_name, prop in input_schema["properties"].items():
55
+ # Create a new property object with required fields
56
+ new_prop = {
57
+ "type": prop.get("type", "string"),
58
+ "description": prop.get("description", f"The {prop_name} parameter")
59
+ }
60
+
61
+ # Copy over any additional fields that might be useful
62
+ for key, value in prop.items():
63
+ if key not in new_prop:
64
+ new_prop[key] = value
65
+
66
+ input_prop_schema["properties"][prop_name] = new_prop
67
+
68
+ # Copy over required fields for input schema
69
+ if "required" in input_schema:
70
+ input_prop_schema["required"] = input_schema["required"]
71
+
72
+ # Add the input property to the main schema
73
+ strict_schema["properties"]["input"] = input_prop_schema
74
+
75
+ # Copy over required fields for main schema
76
+ if "required" in schema:
77
+ strict_schema["required"] = schema["required"]
78
+
79
+ return strict_schema
80
+
81
+ class ToolCall(BaseModel):
82
+ """A tool call from the model."""
83
+ id: str
84
+ name: str
85
+ arguments: dict[str, Any]
86
+
87
+ class Tool(BaseModel):
88
+ """A tool that can be used by the model."""
89
+ name: str
90
+ description: str
91
+ function: Callable[..., Any]
92
+ schema: dict[str, Any]
93
+
94
+ def to_openai_function(self) -> dict[str, Any]:
95
+ """Convert the tool to an OpenAI function definition."""
96
+ # Ensure schema is in strict format
97
+ strict_schema = convert_to_strict_schema(self.schema)
98
+
99
+ return {
100
+ "type": "function",
101
+ "function": {
102
+ "name": self.name,
103
+ "description": self.description,
104
+ "parameters": strict_schema
105
+ }
106
+ }
@@ -292,11 +292,11 @@ wheels = [
292
292
 
293
293
  [[package]]
294
294
  name = "httpx-sse"
295
- version = "0.6.17"
295
+ version = "0.4.0"
296
296
  source = { registry = "https://pypi.org/simple" }
297
- sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.6.17.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
297
+ sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
298
298
  wheels = [
299
- { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.6.17-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
299
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.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.6.17"
449
+ version = "0.6.19"
450
450
  source = { editable = "." }
451
451
  dependencies = [
452
452
  { name = "fastapi" },
@@ -458,6 +458,7 @@ dependencies = [
458
458
  { name = "python-multipart" },
459
459
  { name = "sse-starlette" },
460
460
  { name = "starlette" },
461
+ { name = "typing-inspection" },
461
462
  { name = "uvicorn" },
462
463
  ]
463
464
 
@@ -494,6 +495,7 @@ requires-dist = [
494
495
  { name = "python-multipart", specifier = ">=0.0.20" },
495
496
  { name = "sse-starlette", specifier = ">=2.3.4" },
496
497
  { name = "starlette", specifier = ">=0.46.2" },
498
+ { name = "typing-inspection", specifier = "<=0.4.0" },
497
499
  { name = "uvicorn", specifier = ">=0.34.2" },
498
500
  ]
499
501
  provides-extras = ["dev"]
@@ -980,14 +982,14 @@ wheels = [
980
982
 
981
983
  [[package]]
982
984
  name = "typing-inspection"
983
- version = "0.6.17"
985
+ version = "0.4.0"
984
986
  source = { registry = "https://pypi.org/simple" }
985
987
  dependencies = [
986
988
  { name = "typing-extensions" },
987
989
  ]
988
- sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.6.17.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
990
+ sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
989
991
  wheels = [
990
- { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.6.17-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
992
+ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
991
993
  ]
992
994
 
993
995
  [[package]]
@@ -1,95 +0,0 @@
1
- """
2
- Type definitions for the tools package.
3
- """
4
-
5
- from typing import Any, Callable
6
- from pydantic import BaseModel, Field
7
-
8
- class ToolCall(BaseModel):
9
- """A tool call from the model."""
10
- id: str
11
- name: str
12
- arguments: dict[str, Any]
13
-
14
- class Tool(BaseModel):
15
- """A tool that can be used by the model."""
16
- name: str
17
- description: str
18
- function: Callable[..., Any]
19
- schema: dict[str, Any]
20
-
21
- def to_openai_function(self) -> dict[str, Any]:
22
- """Convert the tool to an OpenAI function definition."""
23
- # Ensure schema is in strict format
24
- strict_schema = self._ensure_strict_schema(self.schema)
25
-
26
- return {
27
- "type": "function",
28
- "function": {
29
- "name": self.name,
30
- "description": self.description,
31
- "parameters": strict_schema
32
- }
33
- }
34
-
35
- def _ensure_strict_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
36
- """Ensure the schema is in strict format required by OpenAI.
37
-
38
- Args:
39
- schema: The input schema to validate and convert
40
-
41
- Returns:
42
- A schema in strict format
43
- """
44
- # If schema has a $ref, resolve it
45
- if "$ref" in schema:
46
- ref = schema["$ref"].split("/")[-1]
47
- schema = schema.get("$defs", {}).get(ref, {})
48
-
49
- # If schema has an input wrapper, unwrap it
50
- if "properties" in schema and "input" in schema["properties"]:
51
- input_schema = schema["properties"]["input"]
52
- if "$ref" in input_schema:
53
- ref = input_schema["$ref"].split("/")[-1]
54
- input_schema = schema.get("$defs", {}).get(ref, {})
55
- schema = input_schema
56
-
57
- # Create a new schema object to ensure we have all required fields
58
- strict_schema = {
59
- "type": "object",
60
- "properties": {},
61
- "required": []
62
- }
63
-
64
- # Copy over properties, ensuring each has type and description
65
- if "properties" in schema:
66
- for prop_name, prop in schema["properties"].items():
67
- # Create a new property object with required fields
68
- new_prop = {
69
- "type": prop.get("type", "string"),
70
- "description": prop.get("description", f"The {prop_name} parameter")
71
- }
72
-
73
- # Copy over any additional fields that might be useful
74
- for key, value in prop.items():
75
- if key not in new_prop:
76
- new_prop[key] = value
77
-
78
- strict_schema["properties"][prop_name] = new_prop
79
-
80
- # Copy over required fields
81
- if "required" in schema:
82
- strict_schema["required"] = schema["required"]
83
-
84
- # Ensure all required fields are actually in properties
85
- strict_schema["required"] = [
86
- req for req in strict_schema["required"]
87
- if req in strict_schema["properties"]
88
- ]
89
-
90
- # Add any additional fields from the original schema
91
- for key, value in schema.items():
92
- if key not in strict_schema:
93
- strict_schema[key] = value
94
-
95
- return strict_schema
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