mbxai 0.6.18__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.18 → mbxai-0.6.19}/PKG-INFO +2 -1
  2. {mbxai-0.6.18 → mbxai-0.6.19}/pyproject.toml +3 -2
  3. {mbxai-0.6.18 → mbxai-0.6.19}/setup.py +1 -1
  4. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/__init__.py +1 -1
  5. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/mcp/client.py +59 -58
  6. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/mcp/server.py +1 -1
  7. {mbxai-0.6.18 → 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.18 → mbxai-0.6.19}/uv.lock +9 -7
  10. mbxai-0.6.18/src/mbxai/tools/types.py +0 -95
  11. {mbxai-0.6.18 → mbxai-0.6.19}/.gitignore +0 -0
  12. {mbxai-0.6.18 → mbxai-0.6.19}/LICENSE +0 -0
  13. {mbxai-0.6.18 → mbxai-0.6.19}/README.md +0 -0
  14. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/core.py +0 -0
  15. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/mcp/__init__.py +0 -0
  16. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/mcp/example.py +0 -0
  17. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/openrouter/__init__.py +0 -0
  18. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/openrouter/client.py +0 -0
  19. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/openrouter/config.py +0 -0
  20. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/openrouter/models.py +0 -0
  21. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/tools/client.py +0 -0
  22. {mbxai-0.6.18 → mbxai-0.6.19}/src/mbxai/tools/example.py +0 -0
  23. {mbxai-0.6.18 → mbxai-0.6.19}/tests/test_core.py +0 -0
  24. {mbxai-0.6.18 → mbxai-0.6.19}/tests/test_mcp.py +0 -0
  25. {mbxai-0.6.18 → mbxai-0.6.19}/tests/test_openrouter.py +0 -0
  26. {mbxai-0.6.18 → 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.18
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.18"
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.18",
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"
@@ -7,7 +7,7 @@ import asyncio
7
7
  import json
8
8
  from pydantic import BaseModel, Field
9
9
 
10
- from ..tools import ToolClient, Tool
10
+ from ..tools import ToolClient, Tool, convert_to_strict_schema
11
11
  from ..openrouter import OpenRouterClient
12
12
 
13
13
  logger = logging.getLogger(__name__)
@@ -22,24 +22,22 @@ class MCPTool(Tool):
22
22
  strict: bool = Field(default=True, description="Whether the tool response is strictly validated")
23
23
  input_schema: dict[str, Any] = Field(description="The input schema for the tool")
24
24
  function: Callable[..., Any] | None = Field(default=None, description="The function that implements the tool")
25
+ _converted_schema: dict[str, Any] | None = None
25
26
 
26
27
  def to_openai_function(self) -> dict[str, Any]:
27
28
  """Convert the tool to an OpenAI function definition."""
28
- # Log the original schema
29
- logger.info(f"Original schema for {self.name}: {json.dumps(self.input_schema, indent=2)}")
30
-
31
- # Convert the schema to strict format
32
- strict_schema = self._convert_to_openai_schema(self.input_schema)
33
-
34
- # Log the converted schema
35
- logger.info(f"Converted schema for {self.name}: {json.dumps(strict_schema, indent=2)}")
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)}")
36
34
 
37
35
  return {
38
36
  "type": "function",
39
37
  "function": {
40
38
  "name": self.name,
41
39
  "description": self.description,
42
- "parameters": strict_schema
40
+ "parameters": self._converted_schema
43
41
  }
44
42
  }
45
43
 
@@ -48,61 +46,64 @@ class MCPTool(Tool):
48
46
  if not mcp_schema:
49
47
  return {"type": "object", "properties": {}, "required": []}
50
48
 
51
- logger.info(f"Starting schema conversion for {self.name}")
52
- logger.info(f"Initial schema: {json.dumps(mcp_schema, indent=2)}")
49
+ # Create a new schema object to ensure we have all required fields
50
+ strict_schema = {
51
+ "type": "object",
52
+ "properties": {},
53
+ "required": []
54
+ }
53
55
 
54
- # If schema has a $ref, resolve it
55
- if "$ref" in mcp_schema:
56
- ref = mcp_schema["$ref"].split("/")[-1]
57
- mcp_schema = mcp_schema.get("$defs", {}).get(ref, {})
58
- logger.info(f"Resolved $ref to: {json.dumps(mcp_schema, indent=2)}")
56
+ # Add additionalProperties: false for strict tools
57
+ if self.strict:
58
+ strict_schema["additionalProperties"] = False
59
59
 
60
- # If schema has an input wrapper, unwrap it
60
+ # Handle input wrapper
61
61
  if "properties" in mcp_schema and "input" in mcp_schema["properties"]:
62
62
  input_schema = mcp_schema["properties"]["input"]
63
+
64
+ # If input has a $ref, resolve it
63
65
  if "$ref" in input_schema:
64
66
  ref = input_schema["$ref"].split("/")[-1]
65
67
  input_schema = mcp_schema.get("$defs", {}).get(ref, {})
66
- mcp_schema = input_schema
67
- logger.info(f"Unwrapped input schema: {json.dumps(mcp_schema, indent=2)}")
68
-
69
- # Create a new schema object to ensure we have all required fields
70
- strict_schema = {
71
- "type": "object",
72
- "properties": {},
73
- "required": []
74
- }
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"]
75
106
 
76
- # Copy over properties, ensuring each has type and description
77
- if "properties" in mcp_schema:
78
- for prop_name, prop in mcp_schema["properties"].items():
79
- # Create a new property object with required fields
80
- new_prop = {
81
- "type": prop.get("type", "string"),
82
- "description": prop.get("description", f"The {prop_name} parameter")
83
- }
84
-
85
- # Copy over any additional fields that might be useful
86
- for key, value in prop.items():
87
- if key not in new_prop:
88
- new_prop[key] = value
89
-
90
- strict_schema["properties"][prop_name] = new_prop
91
- logger.info(f"Added property {prop_name}: {json.dumps(new_prop, indent=2)}")
92
-
93
- # Copy over required fields
94
- if "required" in mcp_schema:
95
- strict_schema["required"] = mcp_schema["required"]
96
- logger.info(f"Added required fields: {strict_schema['required']}")
97
-
98
- # Ensure all required fields are actually in properties
99
- strict_schema["required"] = [
100
- req for req in strict_schema["required"]
101
- if req in strict_schema["properties"]
102
- ]
103
- logger.info(f"Final required fields: {strict_schema['required']}")
104
-
105
- logger.info(f"Final strict schema: {json.dumps(strict_schema, indent=2)}")
106
107
  return strict_schema
107
108
 
108
109
 
@@ -175,7 +176,7 @@ class MCPClient(ToolClient):
175
176
 
176
177
  # Register each tool
177
178
  for idx, tool_data in enumerate(tools_data):
178
- logger.info(f"Processing tool {idx}: {json.dumps(tool_data, indent=2)}")
179
+ logger.debug(f"Processing tool {idx}: {json.dumps(tool_data, indent=2)}")
179
180
 
180
181
  # Ensure tool_data is a dictionary
181
182
  if not isinstance(tool_data, dict):
@@ -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.18"
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.18.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.18-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.18"
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.18"
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.18.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.18-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