mbxai 1.5.0__tar.gz → 1.6.0__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 (33) hide show
  1. {mbxai-1.5.0 → mbxai-1.6.0}/.gitignore +2 -1
  2. {mbxai-1.5.0 → mbxai-1.6.0}/PKG-INFO +1 -1
  3. {mbxai-1.5.0 → mbxai-1.6.0}/pyproject.toml +2 -2
  4. {mbxai-1.5.0 → mbxai-1.6.0}/setup.py +1 -1
  5. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/__init__.py +1 -1
  6. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/mcp/server.py +1 -1
  7. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/tools/types.py +32 -7
  8. mbxai-1.6.0/tests/test_mcp_tool_registration.py +221 -0
  9. mbxai-1.6.0/tests/test_real_mcp_schema.py +308 -0
  10. {mbxai-1.5.0 → mbxai-1.6.0/tests}/test_schema_conversion.py +139 -66
  11. {mbxai-1.5.0 → mbxai-1.6.0}/LICENSE +0 -0
  12. {mbxai-1.5.0 → mbxai-1.6.0}/README.md +0 -0
  13. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/core.py +0 -0
  14. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/mcp/mcp_client_example.py +0 -0
  15. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/mcp/mcp_server_example.py +0 -0
  16. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/openrouter_example.py +0 -0
  17. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/parse_example.py +0 -0
  18. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/parse_tool_example.py +0 -0
  19. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/request.json +0 -0
  20. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/response.json +0 -0
  21. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/send_request.py +0 -0
  22. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/examples/tool_client_example.py +0 -0
  23. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/mcp/__init__.py +0 -0
  24. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/mcp/client.py +0 -0
  25. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/mcp/example.py +0 -0
  26. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/openrouter/__init__.py +0 -0
  27. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/openrouter/client.py +0 -0
  28. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/openrouter/config.py +0 -0
  29. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/openrouter/models.py +0 -0
  30. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/openrouter/schema.py +0 -0
  31. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/tools/__init__.py +0 -0
  32. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/tools/client.py +0 -0
  33. {mbxai-1.5.0 → mbxai-1.6.0}/src/mbxai/tools/example.py +0 -0
@@ -92,4 +92,5 @@ wheels/
92
92
  # Project specific
93
93
  *.db
94
94
  *.sqlite3
95
- node_modules/
95
+ node_modules/
96
+ tmp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.5.0
3
+ Version: 1.6.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mbxai"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  authors = [
9
9
  { name = "MBX AI" }
10
10
  ]
@@ -82,6 +82,6 @@ strict_equality = true
82
82
 
83
83
  [dependency-groups]
84
84
  dev = [
85
- "build>=1.5.0.post1",
85
+ "build>=1.6.0.post1",
86
86
  "twine>=6.1.0",
87
87
  ]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mbxai",
5
- version="1.5.0",
5
+ version="1.6.0",
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__ = "1.5.0"
5
+ __version__ = "1.6.0"
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="1.5.0",
34
+ version="1.6.0",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -18,7 +18,7 @@ def _resolve_ref(schema: dict[str, Any], ref: str, root_schema: dict[str, Any])
18
18
  root_schema: The root schema containing $defs
19
19
 
20
20
  Returns:
21
- The resolved schema definition
21
+ The resolved schema definition with all nested $refs also resolved
22
22
  """
23
23
  if ref.startswith("#/"):
24
24
  # Remove the "#/" prefix and split the path
@@ -33,7 +33,11 @@ def _resolve_ref(schema: dict[str, Any], ref: str, root_schema: dict[str, Any])
33
33
  logger.warning(f"Could not resolve $ref: {ref}")
34
34
  return {"type": "object", "additionalProperties": False}
35
35
 
36
- return current if isinstance(current, dict) else {"type": "object", "additionalProperties": False}
36
+ if isinstance(current, dict):
37
+ # Recursively process the resolved schema to handle nested $refs
38
+ return _convert_property_to_strict(current, root_schema)
39
+ else:
40
+ return {"type": "object", "additionalProperties": False}
37
41
  else:
38
42
  logger.warning(f"Unsupported $ref format: {ref}")
39
43
  return {"type": "object", "additionalProperties": False}
@@ -46,12 +50,32 @@ def _convert_property_to_strict(prop: dict[str, Any], root_schema: dict[str, Any
46
50
  root_schema: The root schema containing $defs
47
51
 
48
52
  Returns:
49
- A strict format property definition
53
+ A strict format property definition with all $refs resolved
50
54
  """
51
- # Handle $ref resolution
55
+ # Handle $ref resolution first - this may return a completely different schema
52
56
  if "$ref" in prop:
53
- resolved = _resolve_ref(prop, prop["$ref"], root_schema)
54
- return _convert_property_to_strict(resolved, root_schema)
57
+ # Get the referenced schema path
58
+ ref = prop["$ref"]
59
+ if ref.startswith("#/"):
60
+ path_parts = ref[2:].split("/")
61
+ current = root_schema
62
+
63
+ # Navigate through the path to get the referenced schema
64
+ for part in path_parts:
65
+ if isinstance(current, dict) and part in current:
66
+ current = current[part]
67
+ else:
68
+ logger.warning(f"Could not resolve $ref: {ref}")
69
+ return {"type": "object", "additionalProperties": False}
70
+
71
+ if isinstance(current, dict):
72
+ # Recursively process the resolved schema
73
+ return _convert_property_to_strict(current, root_schema)
74
+ else:
75
+ return {"type": "object", "additionalProperties": False}
76
+ else:
77
+ logger.warning(f"Unsupported $ref format: {ref}")
78
+ return {"type": "object", "additionalProperties": False}
55
79
 
56
80
  # Get the property type, defaulting to string
57
81
  prop_type = prop.get("type", "string")
@@ -69,7 +93,7 @@ def _convert_property_to_strict(prop: dict[str, Any], root_schema: dict[str, Any
69
93
  new_prop["properties"] = {}
70
94
  new_prop["required"] = []
71
95
 
72
- # Process nested properties
96
+ # Process nested properties recursively
73
97
  if "properties" in prop:
74
98
  for nested_name, nested_prop in prop["properties"].items():
75
99
  new_prop["properties"][nested_name] = _convert_property_to_strict(nested_prop, root_schema)
@@ -81,6 +105,7 @@ def _convert_property_to_strict(prop: dict[str, Any], root_schema: dict[str, Any
81
105
  elif prop_type == "array":
82
106
  # Arrays must have items definition
83
107
  if "items" in prop:
108
+ # Recursively process items schema (this could also have $refs)
84
109
  new_prop["items"] = _convert_property_to_strict(prop["items"], root_schema)
85
110
  else:
86
111
  # Default items schema if missing
@@ -0,0 +1,221 @@
1
+ """
2
+ Test MCP tool registration flow to identify where $ref schemas are still used.
3
+
4
+ This test simulates the server-client registration process to find the bug.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from typing import Any
10
+ from pydantic import BaseModel, Field
11
+
12
+ # Add the src directory to the path so we can import from mbxai
13
+ import sys
14
+ from pathlib import Path
15
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
16
+
17
+ from mbxai.mcp.client import MCPTool
18
+ from mbxai.tools.types import convert_to_strict_schema
19
+
20
+
21
+ class MetadataFilter(BaseModel):
22
+ """Model for a single metadata filter key-value pair."""
23
+ key: str = Field(description="The metadata field name to filter by")
24
+ value: Any = Field(description="The value to filter for")
25
+
26
+
27
+ class ShopwareKnowledgeSearchInput(BaseModel):
28
+ """Input model for Shopware knowledge search."""
29
+
30
+ query: str = Field(description="The search query")
31
+ max_results: int = Field(description="Maximum results (1-20)", ge=1, le=20)
32
+ include_metadata: bool = Field(description="Whether to include metadata")
33
+ metadata_filter: list[MetadataFilter] = Field(description="List of metadata filters")
34
+
35
+
36
+ def test_mcp_tool_registration_flow():
37
+ """Test the exact MCP tool registration flow to identify $ref issues."""
38
+
39
+ print("🔍 Testing MCP Tool Registration Flow")
40
+ print("=" * 50)
41
+
42
+ # Step 1: Simulate what FastMCP generates (like what MCP server gets)
43
+ print("\n1. Simulating FastMCP schema generation...")
44
+ fastmcp_schema = ShopwareKnowledgeSearchInput.model_json_schema()
45
+
46
+ print("FastMCP Raw Schema:")
47
+ print(json.dumps(fastmcp_schema, indent=2))
48
+
49
+ # Create output directory
50
+ tmp_dir = Path(__file__).parent.parent / "tmp"
51
+ tmp_dir.mkdir(exist_ok=True)
52
+
53
+ # Save FastMCP schema
54
+ with open(tmp_dir / "fastmcp_raw_schema.json", 'w') as f:
55
+ json.dump(fastmcp_schema, f, indent=2)
56
+
57
+ # Step 2: Simulate what MCP server stores (what goes in Tool.inputSchema)
58
+ print("\n2. What MCP server stores in Tool.inputSchema...")
59
+ mcp_server_stored_schema = fastmcp_schema # Server just stores the raw schema
60
+
61
+ with open(tmp_dir / "mcp_server_stored_schema.json", 'w') as f:
62
+ json.dump(mcp_server_stored_schema, f, indent=2)
63
+
64
+ # Step 3: Simulate what /tools endpoint returns
65
+ print("\n3. What /tools endpoint returns...")
66
+ tools_endpoint_response = {
67
+ "name": "search_shopware_knowledge",
68
+ "description": "Search Shopware knowledge base",
69
+ "inputSchema": mcp_server_stored_schema, # This has $ref!
70
+ "internal_url": "http://localhost:8000/tools/search_shopware_knowledge/invoke",
71
+ "service": "shopware-search",
72
+ "strict": True
73
+ }
74
+
75
+ print("Tools endpoint response:")
76
+ print(json.dumps(tools_endpoint_response, indent=2))
77
+
78
+ with open(tmp_dir / "tools_endpoint_response.json", 'w') as f:
79
+ json.dump(tools_endpoint_response, f, indent=2)
80
+
81
+ # Step 4: Simulate MCPClient creating MCPTool
82
+ print("\n4. MCPClient creating MCPTool...")
83
+ try:
84
+ # This is what MCPClient.register_mcp_server() does
85
+ mcp_tool = MCPTool(**tools_endpoint_response)
86
+
87
+ print("✅ MCPTool created successfully")
88
+ print(f"MCPTool.inputSchema: {type(mcp_tool.inputSchema)}")
89
+
90
+ # Check if inputSchema still has $ref
91
+ schema_str = json.dumps(mcp_tool.inputSchema)
92
+ has_ref = "$ref" in schema_str or "$defs" in schema_str
93
+ print(f"❌ MCPTool.inputSchema still has $ref/$defs: {has_ref}")
94
+
95
+ if has_ref:
96
+ print(" 🚨 PROBLEM: Raw schema with $ref is stored in MCPTool!")
97
+
98
+ with open(tmp_dir / "mcp_tool_input_schema.json", 'w') as f:
99
+ json.dump(mcp_tool.inputSchema, f, indent=2)
100
+
101
+ except Exception as e:
102
+ print(f"❌ Failed to create MCPTool: {e}")
103
+ return
104
+
105
+ # Step 5: Test MCPTool.to_openai_function()
106
+ print("\n5. Testing MCPTool.to_openai_function()...")
107
+ try:
108
+ openai_function = mcp_tool.to_openai_function()
109
+
110
+ print("✅ OpenAI function created")
111
+
112
+ # Check if the converted schema still has $ref
113
+ function_schema_str = json.dumps(openai_function["function"]["parameters"])
114
+ has_ref_after = "$ref" in function_schema_str or "$defs" in function_schema_str
115
+
116
+ print(f"❌ Final OpenAI schema still has $ref/$defs: {has_ref_after}")
117
+
118
+ if has_ref_after:
119
+ print(" 🚨 CRITICAL PROBLEM: convert_to_strict_schema didn't resolve $ref!")
120
+ else:
121
+ print(" ✅ convert_to_strict_schema successfully resolved $ref")
122
+
123
+ # Check if arrays have items
124
+ def check_arrays_in_schema(schema, path=""):
125
+ issues = []
126
+ if isinstance(schema, dict):
127
+ if schema.get("type") == "array":
128
+ if "items" not in schema:
129
+ issues.append(f"Array at {path} missing items")
130
+ else:
131
+ print(f" ✅ Array at {path} has items")
132
+
133
+ for key, value in schema.items():
134
+ if key == "properties" and isinstance(value, dict):
135
+ for prop_name, prop_schema in value.items():
136
+ issues.extend(check_arrays_in_schema(prop_schema, f"{path}.{prop_name}" if path else prop_name))
137
+ elif key == "items" and isinstance(value, dict):
138
+ issues.extend(check_arrays_in_schema(value, f"{path}.items"))
139
+ return issues
140
+
141
+ array_issues = check_arrays_in_schema(openai_function["function"]["parameters"])
142
+ if array_issues:
143
+ for issue in array_issues:
144
+ print(f" ❌ {issue}")
145
+
146
+ with open(tmp_dir / "final_openai_function.json", 'w') as f:
147
+ json.dump(openai_function, f, indent=2)
148
+
149
+ print(f"\n📁 Files created in {tmp_dir}:")
150
+ print(" - fastmcp_raw_schema.json")
151
+ print(" - mcp_server_stored_schema.json")
152
+ print(" - tools_endpoint_response.json")
153
+ print(" - mcp_tool_input_schema.json")
154
+ print(" - final_openai_function.json")
155
+
156
+ return openai_function
157
+
158
+ except Exception as e:
159
+ print(f"❌ Failed to create OpenAI function: {e}")
160
+ import traceback
161
+ traceback.print_exc()
162
+ return None
163
+
164
+ def test_direct_schema_conversion():
165
+ """Test direct schema conversion to compare with MCP flow."""
166
+
167
+ print("\n\n🔧 Testing Direct Schema Conversion (for comparison)")
168
+ print("=" * 55)
169
+
170
+ # Generate the schema
171
+ schema = ShopwareKnowledgeSearchInput.model_json_schema()
172
+
173
+ print("\n1. Direct conversion without input wrapper...")
174
+ strict_schema = convert_to_strict_schema(schema, strict=True, keep_input_wrapper=False)
175
+
176
+ schema_str = json.dumps(strict_schema)
177
+ has_ref = "$ref" in schema_str or "$defs" in schema_str
178
+ print(f" Direct conversion has $ref/$defs: {has_ref}")
179
+
180
+ print("\n2. Direct conversion with input wrapper...")
181
+ # Simulate MCP-style wrapper
182
+ wrapped_schema = {
183
+ "type": "object",
184
+ "properties": {
185
+ "input": schema
186
+ },
187
+ "required": ["input"]
188
+ }
189
+
190
+ strict_wrapped = convert_to_strict_schema(wrapped_schema, strict=True, keep_input_wrapper=True)
191
+
192
+ wrapped_str = json.dumps(strict_wrapped)
193
+ has_ref_wrapped = "$ref" in wrapped_str or "$defs" in wrapped_str
194
+ print(f" Wrapped conversion has $ref/$defs: {has_ref_wrapped}")
195
+
196
+ # Save for comparison
197
+ tmp_dir = Path(__file__).parent.parent / "tmp"
198
+ with open(tmp_dir / "direct_conversion_no_wrapper.json", 'w') as f:
199
+ json.dump(strict_schema, f, indent=2)
200
+
201
+ with open(tmp_dir / "direct_conversion_with_wrapper.json", 'w') as f:
202
+ json.dump(strict_wrapped, f, indent=2)
203
+
204
+
205
+ if __name__ == "__main__":
206
+ try:
207
+ # Test the MCP registration flow
208
+ result = test_mcp_tool_registration_flow()
209
+
210
+ # Test direct conversion for comparison
211
+ test_direct_schema_conversion()
212
+
213
+ if result:
214
+ print("\n✅ MCP flow test completed - check the JSON files for details")
215
+ else:
216
+ print("\n❌ MCP flow test failed")
217
+
218
+ except Exception as e:
219
+ print(f"\n💥 Test failed with error: {e}")
220
+ import traceback
221
+ traceback.print_exc()
@@ -0,0 +1,308 @@
1
+ """
2
+ Test with the real MCP schema structure from production logs.
3
+
4
+ This test uses the exact schema structure that was causing the error
5
+ to verify the nested $ref resolution fix works correctly.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
12
+
13
+ from mbxai.mcp.client import MCPTool
14
+ from mbxai.tools.types import convert_to_strict_schema
15
+
16
+
17
+ def test_real_production_schema():
18
+ """Test with the exact schema structure from production logs."""
19
+
20
+ print("🔍 Testing Real Production MCP Schema")
21
+ print("=" * 45)
22
+
23
+ # This is the exact schema structure from your production logs
24
+ real_mcp_tool_data = {
25
+ "description": "Search the Shopware knowledge base for relevant information.\n \n This tool performs semantic search through the Shopware knowledge collection\n to find documentation, guides, and other relevant information based on the query.\n \n Args:\n input: ShopwareKnowledgeSearchInput containing search parameters\n \n Returns:\n ShopwareKnowledgeSearchResponse with search results and metadata\n \n Raises:\n RuntimeError: If the search operation fails\n ",
26
+ "inputSchema": {
27
+ "$defs": {
28
+ "MetadataFilter": {
29
+ "description": "Model for a single metadata filter key-value pair.\n\nEach filter represents one condition to apply to the search.\nCommon filter keys include:\n- category: Content category (e.g., 'api', 'plugins', 'themes')\n- version: Shopware version (e.g., '6.5', '6.6') \n- title: Document title (partial match)\n- optimized: Whether content was AI-optimized (true/false)\n- source_url: Source URL (partial match)\n- optimization_strategy: Strategy used ('enhance_readability', etc.)\n- ai_generated_metadata: Whether metadata was AI-generated (true/false)",
30
+ "properties": {
31
+ "key": {
32
+ "description": "The metadata field name to filter by",
33
+ "title": "Key",
34
+ "type": "string"
35
+ },
36
+ "value": {
37
+ "description": "The value to filter for",
38
+ "title": "Value"
39
+ }
40
+ },
41
+ "required": [
42
+ "key",
43
+ "value"
44
+ ],
45
+ "title": "MetadataFilter",
46
+ "type": "object"
47
+ },
48
+ "ShopwareKnowledgeSearchInput": {
49
+ "description": "Input model for Shopware knowledge search.",
50
+ "properties": {
51
+ "include_metadata": {
52
+ "description": "Whether to include metadata in the search results",
53
+ "title": "Include Metadata",
54
+ "type": "boolean"
55
+ },
56
+ "max_results": {
57
+ "description": "Maximum number of search results to return (1-20)",
58
+ "maximum": 20,
59
+ "minimum": 1,
60
+ "title": "Max Results",
61
+ "type": "integer"
62
+ },
63
+ "metadata_filter": {
64
+ "description": "List of metadata filters to apply to the search. Use empty list [] for no filtering, or specify key-value pairs like [{'key': 'category', 'value': 'api'}, {'key': 'version', 'value': '6.5'}]",
65
+ "items": {
66
+ "$ref": "#/$defs/MetadataFilter" # This is the problematic nested $ref
67
+ },
68
+ "title": "Metadata Filter",
69
+ "type": "array"
70
+ },
71
+ "query": {
72
+ "description": "The search query to find relevant Shopware knowledge and documentation",
73
+ "title": "Query",
74
+ "type": "string"
75
+ }
76
+ },
77
+ "required": [
78
+ "query",
79
+ "max_results",
80
+ "include_metadata",
81
+ "metadata_filter"
82
+ ],
83
+ "title": "ShopwareKnowledgeSearchInput",
84
+ "type": "object"
85
+ }
86
+ },
87
+ "properties": {
88
+ "input": {
89
+ "$ref": "#/$defs/ShopwareKnowledgeSearchInput" # This references the above
90
+ }
91
+ },
92
+ "required": [
93
+ "input"
94
+ ],
95
+ "title": "search_shopware_knowledgeArguments",
96
+ "type": "object"
97
+ },
98
+ "internal_url": "http://shopware-knowledge.mbxai-mcp.svc.cluster.local:5000/tools/search_shopware_knowledge/invoke",
99
+ "name": "search_shopware_knowledge",
100
+ "service": "shopware-knowledge",
101
+ "strict": True
102
+ }
103
+
104
+ print("\n1. Creating MCPTool with real production schema...")
105
+
106
+ try:
107
+ # Create MCPTool (this should work)
108
+ mcp_tool = MCPTool(**real_mcp_tool_data)
109
+ print("✅ MCPTool created successfully")
110
+
111
+ # Verify the raw schema still has $refs
112
+ schema_str = json.dumps(mcp_tool.inputSchema)
113
+ has_refs = "$ref" in schema_str and "$defs" in schema_str
114
+ print(f" Raw inputSchema has $ref/$defs: {has_refs}")
115
+
116
+ if not has_refs:
117
+ print(" ⚠️ Warning: Expected raw schema to have $refs")
118
+
119
+ except Exception as e:
120
+ print(f"❌ Failed to create MCPTool: {e}")
121
+ return False
122
+
123
+ print("\n2. Converting to OpenAI function with nested $ref resolution...")
124
+
125
+ try:
126
+ # This is where the magic happens - convert to OpenAI function
127
+ openai_function = mcp_tool.to_openai_function()
128
+ print("✅ OpenAI function created successfully")
129
+
130
+ # Check if all $refs are resolved
131
+ function_params = openai_function["function"]["parameters"]
132
+ params_str = json.dumps(function_params)
133
+
134
+ has_refs_after = "$ref" in params_str or "$defs" in params_str
135
+ print(f" Final schema has $ref/$defs: {has_refs_after}")
136
+
137
+ if has_refs_after:
138
+ print(" ❌ FAIL: Schema conversion didn't resolve all $refs!")
139
+ return False
140
+ else:
141
+ print(" ✅ SUCCESS: All $refs resolved!")
142
+
143
+ # Check the specific structure we care about
144
+ print("\n3. Validating specific schema structure...")
145
+
146
+ # Should have input property
147
+ if "input" not in function_params["properties"]:
148
+ print(" ❌ Missing 'input' property")
149
+ return False
150
+
151
+ input_schema = function_params["properties"]["input"]
152
+ print(" ✅ Found 'input' property")
153
+
154
+ # Should have metadata_filter array
155
+ if "metadata_filter" not in input_schema["properties"]:
156
+ print(" ❌ Missing 'metadata_filter' in input schema")
157
+ return False
158
+
159
+ metadata_filter = input_schema["properties"]["metadata_filter"]
160
+ print(" ✅ Found 'metadata_filter' property")
161
+
162
+ # Should be array type
163
+ if metadata_filter["type"] != "array":
164
+ print(f" ❌ metadata_filter type is {metadata_filter['type']}, expected 'array'")
165
+ return False
166
+ print(" ✅ metadata_filter is array type")
167
+
168
+ # Should have items property
169
+ if "items" not in metadata_filter:
170
+ print(" ❌ metadata_filter missing 'items' property")
171
+ return False
172
+ print(" ✅ metadata_filter has 'items' property")
173
+
174
+ # Items should be object with key/value properties (no $ref)
175
+ items_schema = metadata_filter["items"]
176
+ if items_schema["type"] != "object":
177
+ print(f" ❌ metadata_filter items type is {items_schema['type']}, expected 'object'")
178
+ return False
179
+ print(" ✅ metadata_filter items is object type")
180
+
181
+ if "key" not in items_schema["properties"] or "value" not in items_schema["properties"]:
182
+ print(" ❌ metadata_filter items missing 'key' or 'value' properties")
183
+ return False
184
+ print(" ✅ metadata_filter items has 'key' and 'value' properties")
185
+
186
+ # Check that items schema has no $ref
187
+ items_str = json.dumps(items_schema)
188
+ if "$ref" in items_str:
189
+ print(" ❌ metadata_filter items still contains $ref")
190
+ return False
191
+ print(" ✅ metadata_filter items has no $ref")
192
+
193
+ # Save the results
194
+ tmp_dir = Path(__file__).parent.parent / "tmp"
195
+ tmp_dir.mkdir(exist_ok=True)
196
+
197
+ with open(tmp_dir / "real_production_schema_raw.json", 'w') as f:
198
+ json.dump(mcp_tool.inputSchema, f, indent=2)
199
+
200
+ with open(tmp_dir / "real_production_schema_converted.json", 'w') as f:
201
+ json.dump(openai_function, f, indent=2)
202
+
203
+ print(f"\n📁 Files saved to {tmp_dir}:")
204
+ print(" - real_production_schema_raw.json")
205
+ print(" - real_production_schema_converted.json")
206
+
207
+ print("\n🎉 ALL TESTS PASSED! Nested $ref resolution works correctly!")
208
+ return True
209
+
210
+ except Exception as e:
211
+ print(f"❌ Failed to convert to OpenAI function: {e}")
212
+ import traceback
213
+ traceback.print_exc()
214
+ return False
215
+
216
+
217
+ def test_direct_conversion_with_nested_refs():
218
+ """Test direct schema conversion with the nested refs structure."""
219
+
220
+ print("\n\n🔧 Testing Direct Conversion with Nested $refs")
221
+ print("=" * 50)
222
+
223
+ # The problematic schema structure
224
+ nested_ref_schema = {
225
+ "$defs": {
226
+ "MetadataFilter": {
227
+ "type": "object",
228
+ "properties": {
229
+ "key": {"type": "string", "description": "Filter key"},
230
+ "value": {"type": "string", "description": "Filter value"}
231
+ },
232
+ "required": ["key", "value"]
233
+ },
234
+ "ShopwareInput": {
235
+ "type": "object",
236
+ "properties": {
237
+ "query": {"type": "string", "description": "Search query"},
238
+ "metadata_filter": {
239
+ "type": "array",
240
+ "description": "Metadata filters",
241
+ "items": {
242
+ "$ref": "#/$defs/MetadataFilter" # Nested $ref
243
+ }
244
+ }
245
+ },
246
+ "required": ["query", "metadata_filter"]
247
+ }
248
+ },
249
+ "type": "object",
250
+ "properties": {
251
+ "input": {
252
+ "$ref": "#/$defs/ShopwareInput" # Top-level $ref
253
+ }
254
+ },
255
+ "required": ["input"]
256
+ }
257
+
258
+ print("Testing direct conversion with keep_input_wrapper=True...")
259
+
260
+ try:
261
+ result = convert_to_strict_schema(nested_ref_schema, strict=True, keep_input_wrapper=True)
262
+
263
+ # Check no $refs remain
264
+ result_str = json.dumps(result)
265
+ has_refs = "$ref" in result_str or "$defs" in result_str
266
+
267
+ print(f" Result has $ref/$defs: {has_refs}")
268
+
269
+ if has_refs:
270
+ print(" ❌ Direct conversion still has $refs")
271
+ return False
272
+ else:
273
+ print(" ✅ Direct conversion resolved all $refs")
274
+
275
+ # Save result
276
+ tmp_dir = Path(__file__).parent.parent / "tmp"
277
+ with open(tmp_dir / "direct_nested_ref_conversion.json", 'w') as f:
278
+ json.dump(result, f, indent=2)
279
+
280
+ return True
281
+
282
+ except Exception as e:
283
+ print(f" ❌ Direct conversion failed: {e}")
284
+ return False
285
+
286
+
287
+ if __name__ == "__main__":
288
+ try:
289
+ print("🧪 Testing Nested $ref Resolution Fix")
290
+ print("=" * 40)
291
+
292
+ # Test with real production schema
293
+ test1_passed = test_real_production_schema()
294
+
295
+ # Test direct conversion
296
+ test2_passed = test_direct_conversion_with_nested_refs()
297
+
298
+ if test1_passed and test2_passed:
299
+ print("\n🎉 ALL TESTS PASSED!")
300
+ print("The nested $ref resolution fix is working correctly!")
301
+ else:
302
+ print("\n❌ SOME TESTS FAILED")
303
+ print("The fix needs more work.")
304
+
305
+ except Exception as e:
306
+ print(f"\n💥 Test suite failed: {e}")
307
+ import traceback
308
+ traceback.print_exc()
@@ -6,10 +6,16 @@ when converting Pydantic models to OpenAI function schemas.
6
6
  """
7
7
 
8
8
  import json
9
+ import os
9
10
  from typing import Any
10
11
  from pydantic import BaseModel, Field
11
12
 
12
- from src.mbxai.tools.types import convert_to_strict_schema
13
+ # Add the src directory to the path so we can import from mbxai
14
+ import sys
15
+ from pathlib import Path
16
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
17
+
18
+ from mbxai.tools.types import convert_to_strict_schema
13
19
 
14
20
 
15
21
  class MetadataFilter(BaseModel):
@@ -49,8 +55,8 @@ class ShopwareKnowledgeSearchInput(BaseModel):
49
55
  )
50
56
 
51
57
 
52
- def test_schema_conversion():
53
- """Test that Pydantic models are properly converted to OpenAI-compatible schemas."""
58
+ def test_shopware_schema_conversion():
59
+ """Test that Shopware knowledge search Pydantic models are properly converted to OpenAI-compatible schemas."""
54
60
 
55
61
  print("🧪 Testing Schema Conversion for Shopware Knowledge Search")
56
62
  print("=" * 60)
@@ -59,62 +65,53 @@ def test_schema_conversion():
59
65
  print("\n1. Generating Pydantic JSON Schema...")
60
66
  pydantic_schema = ShopwareKnowledgeSearchInput.model_json_schema()
61
67
 
62
- print("Original Pydantic Schema:")
63
- print(json.dumps(pydantic_schema, indent=2))
64
-
65
- # Test 1: Convert without input wrapper
66
- print("\n2. Converting to OpenAI strict schema (no input wrapper)...")
67
- strict_schema_no_wrapper = convert_to_strict_schema(
68
+ # Convert to OpenAI strict schema
69
+ print("2. Converting to OpenAI strict schema...")
70
+ strict_schema = convert_to_strict_schema(
68
71
  pydantic_schema,
69
72
  strict=True,
70
73
  keep_input_wrapper=False
71
74
  )
72
75
 
73
- print("Strict Schema (no wrapper):")
74
- print(json.dumps(strict_schema_no_wrapper, indent=2))
75
-
76
- # Test 2: Convert with input wrapper (MCP style)
77
- print("\n3. Converting to OpenAI strict schema (with input wrapper)...")
78
-
79
- # Create MCP-style schema with input wrapper
80
- mcp_style_schema = {
81
- "type": "object",
82
- "properties": {
83
- "input": pydantic_schema
84
- },
85
- "required": ["input"],
86
- "additionalProperties": False
87
- }
88
-
89
- strict_schema_with_wrapper = convert_to_strict_schema(
90
- mcp_style_schema,
91
- strict=True,
92
- keep_input_wrapper=True
93
- )
94
-
95
- print("Strict Schema (with input wrapper):")
96
- print(json.dumps(strict_schema_with_wrapper, indent=2))
97
-
98
- # Test 3: Create OpenAI function definition
99
- print("\n4. Creating OpenAI Function Definition...")
100
-
76
+ # Create OpenAI function definition
77
+ print("3. Creating OpenAI Function Definition...")
101
78
  function_def = {
102
79
  "type": "function",
103
80
  "function": {
104
81
  "name": "search_shopware_knowledge",
105
82
  "description": "Search Shopware knowledge base for relevant documentation and information",
106
- "parameters": strict_schema_no_wrapper,
83
+ "parameters": strict_schema,
107
84
  "strict": True
108
85
  }
109
86
  }
110
87
 
111
- print("OpenAI Function Definition:")
112
- print(json.dumps(function_def, indent=2))
88
+ # Create local tmp directory for output files
89
+ tmp_dir = Path(__file__).parent.parent / "tmp"
90
+ tmp_dir.mkdir(exist_ok=True)
91
+
92
+ # Create output files in local tmp directory
93
+ original_schema_file = tmp_dir / "shopware_original_schema.json"
94
+ strict_schema_file = tmp_dir / "shopware_strict_schema.json"
95
+ function_def_file = tmp_dir / "shopware_openai_function.json"
96
+
97
+ with open(original_schema_file, 'w') as f:
98
+ json.dump(pydantic_schema, f, indent=2)
99
+
100
+ with open(strict_schema_file, 'w') as f:
101
+ json.dump(strict_schema, f, indent=2)
102
+
103
+ with open(function_def_file, 'w') as f:
104
+ json.dump(function_def, f, indent=2)
105
+
106
+ print(f"\n📄 Generated files:")
107
+ print(f" Original Pydantic Schema: {original_schema_file}")
108
+ print(f" OpenAI Strict Schema: {strict_schema_file}")
109
+ print(f" OpenAI Function Definition: {function_def_file}")
113
110
 
114
111
  # Validation checks
115
- print("\n5. Validation Checks...")
116
- print("✅ Checking that all arrays have 'items' property...")
112
+ print("\n4. Validation Checks...")
117
113
 
114
+ # Check that all arrays have 'items' property
118
115
  def check_arrays_have_items(schema, path=""):
119
116
  """Recursively check that all arrays have items property."""
120
117
  issues = []
@@ -136,22 +133,18 @@ def test_schema_conversion():
136
133
 
137
134
  return issues
138
135
 
139
- issues = check_arrays_have_items(strict_schema_no_wrapper)
140
- if issues:
141
- for issue in issues:
142
- print(f" {issue}")
143
- raise AssertionError(f"Schema validation failed: {issues}")
144
- else:
145
- print(" ✓ All arrays have proper 'items' definitions")
136
+ print("✅ Checking that all arrays have 'items' property...")
137
+ issues = check_arrays_have_items(strict_schema)
138
+ assert not issues, f"Schema validation failed: {issues}"
139
+ print(" All arrays have proper 'items' definitions")
146
140
 
141
+ # Check that no $ref or $defs exist
147
142
  print("\n✅ Checking that no $ref or $defs exist...")
148
- schema_str = json.dumps(strict_schema_no_wrapper)
149
- if "$ref" in schema_str or "$defs" in schema_str:
150
- raise AssertionError("Schema still contains $ref or $defs")
151
- else:
152
- print(" ✓ No $ref or $defs found - schema is fully inlined")
143
+ schema_str = json.dumps(strict_schema)
144
+ assert "$ref" not in schema_str and "$defs" not in schema_str, "Schema still contains $ref or $defs"
145
+ print(" No $ref or $defs found - schema is fully inlined")
153
146
 
154
- print("\n✅ Checking that all objects have additionalProperties: false...")
147
+ # Check that all objects have additionalProperties: false
155
148
  def check_additional_properties(schema, path=""):
156
149
  """Recursively check that all objects have additionalProperties: false."""
157
150
  issues = []
@@ -175,13 +168,30 @@ def test_schema_conversion():
175
168
 
176
169
  return issues
177
170
 
178
- issues = check_additional_properties(strict_schema_no_wrapper)
179
- if issues:
180
- for issue in issues:
181
- print(f" {issue}")
182
- raise AssertionError(f"additionalProperties validation failed: {issues}")
183
- else:
184
- print(" All objects have additionalProperties: false")
171
+ print("\n✅ Checking that all objects have additionalProperties: false...")
172
+ issues = check_additional_properties(strict_schema)
173
+ assert not issues, f"additionalProperties validation failed: {issues}"
174
+ print(" All objects have additionalProperties: false")
175
+
176
+ # Check that constraints are preserved
177
+ print("\n✅ Checking that constraints are preserved...")
178
+ max_results_prop = strict_schema["properties"]["max_results"]
179
+ assert max_results_prop["minimum"] == 1, "minimum constraint not preserved"
180
+ assert max_results_prop["maximum"] == 20, "maximum constraint not preserved"
181
+ print(" ✓ Constraints preserved (ge=1, le=20 for max_results)")
182
+
183
+ # Check that metadata_filter array has proper MetadataFilter items
184
+ print("\n✅ Checking metadata_filter array structure...")
185
+ metadata_filter_prop = strict_schema["properties"]["metadata_filter"]
186
+ assert metadata_filter_prop["type"] == "array", "metadata_filter should be array"
187
+ assert "items" in metadata_filter_prop, "metadata_filter array missing items"
188
+
189
+ items_schema = metadata_filter_prop["items"]
190
+ assert items_schema["type"] == "object", "metadata_filter items should be object"
191
+ assert "key" in items_schema["properties"], "metadata_filter items missing 'key' property"
192
+ assert "value" in items_schema["properties"], "metadata_filter items missing 'value' property"
193
+ assert items_schema["required"] == ["key", "value"], "metadata_filter items missing required fields"
194
+ print(" ✓ metadata_filter array has proper MetadataFilter object items")
185
195
 
186
196
  print("\n🎉 All tests passed! Schema is OpenAI/OpenRouter compatible!")
187
197
  print("\nKey improvements:")
@@ -190,15 +200,78 @@ def test_schema_conversion():
190
200
  print("- ✅ All objects have additionalProperties: false")
191
201
  print("- ✅ Constraints preserved (ge=1, le=20 for max_results)")
192
202
  print("- ✅ Complex nested structures handled correctly")
203
+ print("- ✅ metadata_filter array properly defines MetadataFilter items")
193
204
 
194
- return function_def
205
+ # Return file paths for further inspection
206
+ return {
207
+ "original_schema_file": str(original_schema_file),
208
+ "strict_schema_file": str(strict_schema_file),
209
+ "function_def_file": str(function_def_file),
210
+ "function_definition": function_def
211
+ }
212
+
213
+
214
+ def test_mcp_style_schema_conversion():
215
+ """Test schema conversion with MCP-style input wrapper."""
216
+
217
+ print("\n🧪 Testing MCP-Style Schema Conversion")
218
+ print("=" * 50)
219
+
220
+ # Generate the JSON schema from the Pydantic model
221
+ pydantic_schema = ShopwareKnowledgeSearchInput.model_json_schema()
222
+
223
+ # Create MCP-style schema with input wrapper
224
+ mcp_style_schema = {
225
+ "type": "object",
226
+ "properties": {
227
+ "input": pydantic_schema
228
+ },
229
+ "required": ["input"],
230
+ "additionalProperties": False
231
+ }
232
+
233
+ # Convert with input wrapper
234
+ strict_schema_with_wrapper = convert_to_strict_schema(
235
+ mcp_style_schema,
236
+ strict=True,
237
+ keep_input_wrapper=True
238
+ )
239
+
240
+ # Create output file in local tmp directory
241
+ tmp_dir = Path(__file__).parent.parent / "tmp"
242
+ tmp_dir.mkdir(exist_ok=True)
243
+
244
+ mcp_schema_file = tmp_dir / "shopware_mcp_style_schema.json"
245
+ with open(mcp_schema_file, 'w') as f:
246
+ json.dump(strict_schema_with_wrapper, f, indent=2)
247
+
248
+ print(f"📄 MCP-Style Schema: {mcp_schema_file}")
249
+
250
+ # Validate MCP-style structure
251
+ assert "input" in strict_schema_with_wrapper["properties"], "MCP wrapper missing input property"
252
+ input_schema = strict_schema_with_wrapper["properties"]["input"]
253
+ assert "metadata_filter" in input_schema["properties"], "Input schema missing metadata_filter"
254
+
255
+ print("✅ MCP-style schema conversion successful!")
256
+
257
+ return str(mcp_schema_file)
195
258
 
196
259
 
197
260
  if __name__ == "__main__":
198
261
  try:
199
- function_def = test_schema_conversion()
200
- print(f"\n✅ Test completed successfully!")
201
- print(f"Function definition ready for OpenRouter API")
262
+ # Run the main test
263
+ result = test_shopware_schema_conversion()
264
+
265
+ # Run the MCP-style test
266
+ mcp_file = test_mcp_style_schema_conversion()
267
+
268
+ print(f"\n✅ All tests completed successfully!")
269
+ print(f"\n📁 Check these files to inspect the generated schemas:")
270
+ print(f" - Original: {result['original_schema_file']}")
271
+ print(f" - Strict: {result['strict_schema_file']}")
272
+ print(f" - Function: {result['function_def_file']}")
273
+ print(f" - MCP Style: {mcp_file}")
274
+
202
275
  except Exception as e:
203
276
  print(f"\n❌ Test failed: {e}")
204
277
  raise
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