mbxai 1.3.0__py3-none-any.whl → 1.5.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/examples/mcp/mcp_client_example.py +4 -0
- mbxai/mcp/server.py +4 -7
- mbxai/openrouter/client.py +18 -41
- mbxai/tools/types.py +110 -37
- {mbxai-1.3.0.dist-info → mbxai-1.5.0.dist-info}/METADATA +1 -1
- {mbxai-1.3.0.dist-info → mbxai-1.5.0.dist-info}/RECORD +9 -9
- {mbxai-1.3.0.dist-info → mbxai-1.5.0.dist-info}/WHEEL +0 -0
- {mbxai-1.3.0.dist-info → mbxai-1.5.0.dist-info}/licenses/LICENSE +0 -0
mbxai/__init__.py
CHANGED
@@ -51,6 +51,10 @@ def main():
|
|
51
51
|
if not hasattr(response, 'choices'):
|
52
52
|
logger.error(f"Invalid response format - no choices attribute: {response}")
|
53
53
|
return
|
54
|
+
|
55
|
+
if response.choices is None:
|
56
|
+
logger.error("Response choices is None")
|
57
|
+
return
|
54
58
|
|
55
59
|
if not response.choices:
|
56
60
|
logger.error("No choices in response")
|
mbxai/mcp/server.py
CHANGED
@@ -31,7 +31,7 @@ class MCPServer:
|
|
31
31
|
self.app = FastAPI(
|
32
32
|
title=self.name,
|
33
33
|
description=self.description,
|
34
|
-
version="1.
|
34
|
+
version="1.5.0",
|
35
35
|
)
|
36
36
|
|
37
37
|
# Initialize MCP server
|
@@ -74,13 +74,10 @@ class MCPServer:
|
|
74
74
|
tools = await self.mcp_server.list_tools()
|
75
75
|
tool_metadata = tools[-1]
|
76
76
|
|
77
|
-
#
|
77
|
+
# Use the raw inputSchema from FastMCP - it will be processed later by convert_to_strict_schema
|
78
|
+
# when the tool is converted to OpenAI function format
|
78
79
|
inputSchema = tool_metadata.inputSchema
|
79
|
-
|
80
|
-
if "$ref" in inputSchema:
|
81
|
-
ref = inputSchema["$ref"].split("/")[-1]
|
82
|
-
inputSchema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
|
83
|
-
|
80
|
+
|
84
81
|
# Create and store Tool instance
|
85
82
|
self._tools[tool_metadata.name] = Tool(
|
86
83
|
name=tool_metadata.name,
|
mbxai/openrouter/client.py
CHANGED
@@ -234,6 +234,15 @@ class OpenRouterClient:
|
|
234
234
|
logger.error("Received None response from OpenRouter API")
|
235
235
|
raise OpenRouterAPIError("Received None response from OpenRouter API")
|
236
236
|
|
237
|
+
# Validate response structure
|
238
|
+
if not hasattr(response, 'choices'):
|
239
|
+
logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
|
240
|
+
raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
|
241
|
+
|
242
|
+
if response.choices is None:
|
243
|
+
logger.error("Response choices is None")
|
244
|
+
raise OpenRouterAPIError("Invalid response format: choices is None")
|
245
|
+
|
237
246
|
logger.debug(f"Response type: {type(response)}")
|
238
247
|
logger.debug(f"Response attributes: {dir(response)}")
|
239
248
|
logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
|
@@ -320,47 +329,6 @@ class OpenRouterClient:
|
|
320
329
|
logger.error("Received None response from OpenRouter API")
|
321
330
|
raise OpenRouterAPIError("Received None response from OpenRouter API")
|
322
331
|
|
323
|
-
# Try to get the raw response content if available
|
324
|
-
if hasattr(response, '_response'):
|
325
|
-
try:
|
326
|
-
raw_content = response._response.text
|
327
|
-
logger.debug(f"Raw response content: {raw_content[:1000]}...")
|
328
|
-
except Exception as e:
|
329
|
-
logger.debug(f"Could not get raw response content: {e}")
|
330
|
-
|
331
|
-
# Validate response structure
|
332
|
-
if not hasattr(response, 'choices'):
|
333
|
-
logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
|
334
|
-
raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
|
335
|
-
|
336
|
-
if not response.choices:
|
337
|
-
logger.error("Response has empty choices list")
|
338
|
-
raise OpenRouterAPIError("Invalid response format: empty choices list")
|
339
|
-
|
340
|
-
if not hasattr(response.choices[0], 'message'):
|
341
|
-
logger.error(f"First choice missing 'message' attribute. Available attributes: {dir(response.choices[0])}")
|
342
|
-
raise OpenRouterAPIError("Invalid response format: missing 'message' attribute in first choice")
|
343
|
-
|
344
|
-
# Check if the message has a parsed attribute or content
|
345
|
-
if not hasattr(response.choices[0].message, 'parsed') and not hasattr(response.choices[0].message, 'content'):
|
346
|
-
logger.error(f"Message missing both 'parsed' and 'content' attributes. Available attributes: {dir(response.choices[0].message)}")
|
347
|
-
raise OpenRouterAPIError("Invalid response format: message must have either 'parsed' or 'content' attribute")
|
348
|
-
|
349
|
-
# If there's no parsed attribute but there is content, try to parse it
|
350
|
-
if not hasattr(response.choices[0].message, 'parsed') and hasattr(response.choices[0].message, 'content'):
|
351
|
-
try:
|
352
|
-
content = response.choices[0].message.content
|
353
|
-
if isinstance(content, str):
|
354
|
-
parsed = json.loads(content)
|
355
|
-
response.choices[0].message.parsed = parsed
|
356
|
-
else:
|
357
|
-
response.choices[0].message.parsed = content
|
358
|
-
except Exception as e:
|
359
|
-
stack_trace = traceback.format_exc()
|
360
|
-
logger.error(f"Failed to parse message content: {str(e)}")
|
361
|
-
logger.error(f"Stack trace:\n{stack_trace}")
|
362
|
-
raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}\nStack trace:\n{stack_trace}")
|
363
|
-
|
364
332
|
logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
|
365
333
|
|
366
334
|
return response
|
@@ -431,6 +399,15 @@ class OpenRouterClient:
|
|
431
399
|
logger.error("Received None response from OpenRouter API")
|
432
400
|
raise OpenRouterAPIError("Received None response from OpenRouter API")
|
433
401
|
|
402
|
+
# Validate response structure
|
403
|
+
if not hasattr(response, 'choices'):
|
404
|
+
logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
|
405
|
+
raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
|
406
|
+
|
407
|
+
if response.choices is None:
|
408
|
+
logger.error("Response choices is None")
|
409
|
+
raise OpenRouterAPIError("Invalid response format: choices is None")
|
410
|
+
|
434
411
|
logger.debug(f"Response type: {type(response)}")
|
435
412
|
logger.debug(f"Response attributes: {dir(response)}")
|
436
413
|
logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
|
mbxai/tools/types.py
CHANGED
@@ -9,16 +9,117 @@ import json
|
|
9
9
|
|
10
10
|
logger = logging.getLogger(__name__)
|
11
11
|
|
12
|
+
def _resolve_ref(schema: dict[str, Any], ref: str, root_schema: dict[str, Any]) -> dict[str, Any]:
|
13
|
+
"""Resolve a $ref to its actual schema definition.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
schema: The current schema object
|
17
|
+
ref: The reference string (e.g., "#/$defs/MyType")
|
18
|
+
root_schema: The root schema containing $defs
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
The resolved schema definition
|
22
|
+
"""
|
23
|
+
if ref.startswith("#/"):
|
24
|
+
# Remove the "#/" prefix and split the path
|
25
|
+
path_parts = ref[2:].split("/")
|
26
|
+
current = root_schema
|
27
|
+
|
28
|
+
# Navigate through the path
|
29
|
+
for part in path_parts:
|
30
|
+
if isinstance(current, dict) and part in current:
|
31
|
+
current = current[part]
|
32
|
+
else:
|
33
|
+
logger.warning(f"Could not resolve $ref: {ref}")
|
34
|
+
return {"type": "object", "additionalProperties": False}
|
35
|
+
|
36
|
+
return current if isinstance(current, dict) else {"type": "object", "additionalProperties": False}
|
37
|
+
else:
|
38
|
+
logger.warning(f"Unsupported $ref format: {ref}")
|
39
|
+
return {"type": "object", "additionalProperties": False}
|
40
|
+
|
41
|
+
def _convert_property_to_strict(prop: dict[str, Any], root_schema: dict[str, Any]) -> dict[str, Any]:
|
42
|
+
"""Convert a single property to strict format, recursively handling nested objects and arrays.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
prop: The property definition to convert
|
46
|
+
root_schema: The root schema containing $defs
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
A strict format property definition
|
50
|
+
"""
|
51
|
+
# Handle $ref resolution
|
52
|
+
if "$ref" in prop:
|
53
|
+
resolved = _resolve_ref(prop, prop["$ref"], root_schema)
|
54
|
+
return _convert_property_to_strict(resolved, root_schema)
|
55
|
+
|
56
|
+
# Get the property type, defaulting to string
|
57
|
+
prop_type = prop.get("type", "string")
|
58
|
+
|
59
|
+
# Start with basic property structure
|
60
|
+
new_prop = {
|
61
|
+
"type": prop_type,
|
62
|
+
"description": prop.get("description", f"A {prop_type} parameter")
|
63
|
+
}
|
64
|
+
|
65
|
+
# Handle specific types
|
66
|
+
if prop_type == "object":
|
67
|
+
# Objects need additionalProperties: false and properties
|
68
|
+
new_prop["additionalProperties"] = False
|
69
|
+
new_prop["properties"] = {}
|
70
|
+
new_prop["required"] = []
|
71
|
+
|
72
|
+
# Process nested properties
|
73
|
+
if "properties" in prop:
|
74
|
+
for nested_name, nested_prop in prop["properties"].items():
|
75
|
+
new_prop["properties"][nested_name] = _convert_property_to_strict(nested_prop, root_schema)
|
76
|
+
|
77
|
+
# Copy required fields
|
78
|
+
if "required" in prop:
|
79
|
+
new_prop["required"] = prop["required"]
|
80
|
+
|
81
|
+
elif prop_type == "array":
|
82
|
+
# Arrays must have items definition
|
83
|
+
if "items" in prop:
|
84
|
+
new_prop["items"] = _convert_property_to_strict(prop["items"], root_schema)
|
85
|
+
else:
|
86
|
+
# Default items schema if missing
|
87
|
+
new_prop["items"] = {
|
88
|
+
"type": "string",
|
89
|
+
"description": "Array item"
|
90
|
+
}
|
91
|
+
|
92
|
+
elif prop_type in ["string", "number", "integer", "boolean"]:
|
93
|
+
# For primitive types, copy additional constraints
|
94
|
+
if "enum" in prop:
|
95
|
+
new_prop["enum"] = prop["enum"]
|
96
|
+
if "minimum" in prop:
|
97
|
+
new_prop["minimum"] = prop["minimum"]
|
98
|
+
if "maximum" in prop:
|
99
|
+
new_prop["maximum"] = prop["maximum"]
|
100
|
+
if "pattern" in prop:
|
101
|
+
new_prop["pattern"] = prop["pattern"]
|
102
|
+
if "format" in prop:
|
103
|
+
new_prop["format"] = prop["format"]
|
104
|
+
|
105
|
+
return new_prop
|
106
|
+
|
12
107
|
def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_input_wrapper: bool = False) -> dict[str, Any]:
|
13
108
|
"""Convert a schema to strict format required by OpenAI.
|
14
109
|
|
110
|
+
This function handles:
|
111
|
+
- Resolving all $ref and $defs to inline definitions
|
112
|
+
- Adding required 'items' property for arrays
|
113
|
+
- Ensuring all objects have 'additionalProperties: false'
|
114
|
+
- Recursively processing nested schemas
|
115
|
+
|
15
116
|
Args:
|
16
117
|
schema: The input schema to validate and convert
|
17
118
|
strict: Whether to enforce strict validation with additionalProperties: false
|
18
119
|
keep_input_wrapper: Whether to keep the input wrapper (for MCP tools)
|
19
120
|
|
20
121
|
Returns:
|
21
|
-
A schema in strict format
|
122
|
+
A schema in strict format with all references resolved
|
22
123
|
"""
|
23
124
|
if not schema:
|
24
125
|
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
@@ -31,14 +132,16 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
|
|
31
132
|
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
32
133
|
}
|
33
134
|
|
135
|
+
# Store the root schema for $ref resolution
|
136
|
+
root_schema = schema
|
137
|
+
|
34
138
|
# Handle input wrapper
|
35
139
|
if "properties" in schema and "input" in schema["properties"]:
|
36
140
|
inputSchema = schema["properties"]["input"]
|
37
141
|
|
38
142
|
# If input has a $ref, resolve it
|
39
143
|
if "$ref" in inputSchema:
|
40
|
-
|
41
|
-
inputSchema = schema.get("$defs", {}).get(ref, {})
|
144
|
+
inputSchema = _resolve_ref(inputSchema, inputSchema["$ref"], root_schema)
|
42
145
|
|
43
146
|
if keep_input_wrapper:
|
44
147
|
# Create the input property schema
|
@@ -49,20 +152,10 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
|
|
49
152
|
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
50
153
|
}
|
51
154
|
|
52
|
-
#
|
155
|
+
# Process input properties
|
53
156
|
if "properties" in inputSchema:
|
54
157
|
for prop_name, prop in inputSchema["properties"].items():
|
55
|
-
|
56
|
-
new_prop = {
|
57
|
-
"type": prop.get("type", "string"),
|
58
|
-
"description": prop.get("description", f"The {prop_name} parameter")
|
59
|
-
}
|
60
|
-
|
61
|
-
# If the property is an object, ensure it has additionalProperties: false
|
62
|
-
if new_prop["type"] == "object":
|
63
|
-
new_prop["additionalProperties"] = False
|
64
|
-
|
65
|
-
input_prop_schema["properties"][prop_name] = new_prop
|
158
|
+
input_prop_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
66
159
|
|
67
160
|
# Copy over required fields for input schema
|
68
161
|
if "required" in inputSchema:
|
@@ -78,17 +171,7 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
|
|
78
171
|
# If not keeping input wrapper, use input schema directly
|
79
172
|
if "properties" in inputSchema:
|
80
173
|
for prop_name, prop in inputSchema["properties"].items():
|
81
|
-
|
82
|
-
new_prop = {
|
83
|
-
"type": prop.get("type", "string"),
|
84
|
-
"description": prop.get("description", f"The {prop_name} parameter")
|
85
|
-
}
|
86
|
-
|
87
|
-
# If the property is an object, ensure it has additionalProperties: false
|
88
|
-
if new_prop["type"] == "object":
|
89
|
-
new_prop["additionalProperties"] = False
|
90
|
-
|
91
|
-
strict_schema["properties"][prop_name] = new_prop
|
174
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
92
175
|
|
93
176
|
# Copy over required fields
|
94
177
|
if "required" in inputSchema:
|
@@ -97,17 +180,7 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
|
|
97
180
|
# If no input wrapper, use the schema as is
|
98
181
|
if "properties" in schema:
|
99
182
|
for prop_name, prop in schema["properties"].items():
|
100
|
-
|
101
|
-
new_prop = {
|
102
|
-
"type": prop.get("type", "string"),
|
103
|
-
"description": prop.get("description", f"The {prop_name} parameter")
|
104
|
-
}
|
105
|
-
|
106
|
-
# If the property is an object, ensure it has additionalProperties: false
|
107
|
-
if new_prop["type"] == "object":
|
108
|
-
new_prop["additionalProperties"] = False
|
109
|
-
|
110
|
-
strict_schema["properties"][prop_name] = new_prop
|
183
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
111
184
|
|
112
185
|
# Copy over required fields
|
113
186
|
if "required" in schema:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
mbxai/__init__.py,sha256=
|
1
|
+
mbxai/__init__.py,sha256=mTVWACpiOVn0-b8RkDKds5fidOixUuIoeSMF4yk9_ug,47
|
2
2
|
mbxai/core.py,sha256=WMvmU9TTa7M_m-qWsUew4xH8Ul6xseCZ2iBCXJTW-Bs,196
|
3
3
|
mbxai/examples/openrouter_example.py,sha256=-grXHKMmFLoh-yUIEMc31n8Gg1S7uSazBWCIOWxgbyQ,1317
|
4
4
|
mbxai/examples/parse_example.py,sha256=eCKMJoOl6qwo8sDP6Trc6ncgjPlgTqi5tPE2kB5_P0k,3821
|
@@ -7,22 +7,22 @@ mbxai/examples/request.json,sha256=fjVMses305wVUXgcmjESCvPgP81Js8Kk6zHjZ8EDyEg,5
|
|
7
7
|
mbxai/examples/response.json,sha256=4SGJJyQjWWeN__Mrxm6ZtHIo1NUtLEheldd5KaA2mHw,856
|
8
8
|
mbxai/examples/send_request.py,sha256=O5gCHUHy7RvkEFo9IQATgnSOfOdu8OqKHfjAlLDwWPg,6023
|
9
9
|
mbxai/examples/tool_client_example.py,sha256=9DNaejXLA85dPbExMiv5y76qlFhzOJF9E5EnMOsy_Dc,3993
|
10
|
-
mbxai/examples/mcp/mcp_client_example.py,sha256=
|
10
|
+
mbxai/examples/mcp/mcp_client_example.py,sha256=d5-TRHNDdp3nT_NGt0tKpT3VUAJVvqAHSyqkzk9Dd2s,2972
|
11
11
|
mbxai/examples/mcp/mcp_server_example.py,sha256=nFfg22Jnc6HMW_ezLO3So1xwDdx2_rItj5CR-y_Nevs,3966
|
12
12
|
mbxai/mcp/__init__.py,sha256=_ek9iYdYqW5saKetj4qDci11jxesQDiHPJRpHMKkxgU,175
|
13
13
|
mbxai/mcp/client.py,sha256=QRzId6o4_WRWVv3rtm8cfZZGaoY_UlaOO-oqNjY-tmw,5219
|
14
14
|
mbxai/mcp/example.py,sha256=oaol7AvvZnX86JWNz64KvPjab5gg1VjVN3G8eFSzuaE,2350
|
15
|
-
mbxai/mcp/server.py,sha256
|
15
|
+
mbxai/mcp/server.py,sha256=-dzINI4cqzGuGyenXK1Jm9Tq5KS1l59x36Pe0KYgWVc,3332
|
16
16
|
mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
|
17
|
-
mbxai/openrouter/client.py,sha256=
|
17
|
+
mbxai/openrouter/client.py,sha256=3LD6WDJ8wjo_nefH5d1NJCsrWPvBc_KBf2NsItUoSt8,18302
|
18
18
|
mbxai/openrouter/config.py,sha256=Ia93s-auim9Sq71eunVDbn9ET5xX2zusXpV4JBdHAzs,3251
|
19
19
|
mbxai/openrouter/models.py,sha256=b3IjjtZAjeGOf2rLsdnCD1HacjTnS8jmv_ZXorc-KJQ,2604
|
20
20
|
mbxai/openrouter/schema.py,sha256=H_77ZrA9zmbX155bWpCJj1jehUyJPS0QybEW1IVAoe0,540
|
21
21
|
mbxai/tools/__init__.py,sha256=ogxrHvgJ7OR62Lmd5x9Eh5d2C0jqWyQis7Zy3yKpZ78,218
|
22
22
|
mbxai/tools/client.py,sha256=j6yB2hYxvWbaQ5SqN1Fs_YFdPtwettdcMoXcdeV-520,14930
|
23
23
|
mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
|
24
|
-
mbxai/tools/types.py,sha256=
|
25
|
-
mbxai-1.
|
26
|
-
mbxai-1.
|
27
|
-
mbxai-1.
|
28
|
-
mbxai-1.
|
24
|
+
mbxai/tools/types.py,sha256=AIYaX7onY6eRcCEUjrymtiSYFYXi4hpVXLYbj-i9qqo,8277
|
25
|
+
mbxai-1.5.0.dist-info/METADATA,sha256=d55xUlW3m4tMaz6Fs2dKIWc4TnGcqfuI0jdWe9GkBOY,4147
|
26
|
+
mbxai-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
27
|
+
mbxai-1.5.0.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
|
28
|
+
mbxai-1.5.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|