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 CHANGED
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "1.3.0"
5
+ __version__ = "1.5.0"
@@ -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.3.0",
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
- # Convert FastMCP schema to our schema format
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
- if isinstance(inputSchema, dict):
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,
@@ -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
- ref = inputSchema["$ref"].split("/")[-1]
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
- # Copy over input properties
155
+ # Process input properties
53
156
  if "properties" in inputSchema:
54
157
  for prop_name, prop in inputSchema["properties"].items():
55
- # Create a new property object with only allowed fields
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
- # Create a new property object with only allowed fields
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
- # Create a new property object with only allowed fields
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.3.0
3
+ Version: 1.5.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
@@ -1,4 +1,4 @@
1
- mbxai/__init__.py,sha256=4cotIuKStlHqnJBxOJuEwoHh2V9cg49no_NLkaElFts,47
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=R4H-OU5FvGL41cCkTdLa3bocsmVJYQYOcOHRf61nbZc,2822
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=k_MH77DHutAfnWUSt2UFxoycqH6xn33bZOhdM5PTuW0,3454
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=wWdYIzbrG-0c_MkK2GYy92OOmaaCKVv7qGUwU5tqtTo,19893
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=7gNIJBjzr9i4DT50OGLMjn3-6yBXqlK-kIz_RWcqywo,5875
25
- mbxai-1.3.0.dist-info/METADATA,sha256=j9nZGvIPsVhcBL281HSyZNIOEorCboE0qtSfsZqVbSc,4147
26
- mbxai-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- mbxai-1.3.0.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
28
- mbxai-1.3.0.dist-info/RECORD,,
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