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