mbxai 1.4.0__tar.gz → 1.5.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.
- {mbxai-1.4.0 → mbxai-1.5.0}/PKG-INFO +1 -1
- {mbxai-1.4.0 → mbxai-1.5.0}/pyproject.toml +2 -2
- {mbxai-1.4.0 → mbxai-1.5.0}/setup.py +1 -1
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/__init__.py +1 -1
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/mcp/server.py +4 -7
- mbxai-1.5.0/src/mbxai/tools/types.py +220 -0
- mbxai-1.5.0/test_schema_conversion.py +204 -0
- mbxai-1.4.0/src/mbxai/tools/types.py +0 -147
- {mbxai-1.4.0 → mbxai-1.5.0}/.gitignore +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/LICENSE +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/README.md +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/core.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/mcp/mcp_client_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/mcp/mcp_server_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/openrouter_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/parse_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/parse_tool_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/request.json +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/response.json +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/send_request.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/examples/tool_client_example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/mcp/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/mcp/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/mcp/example.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/openrouter/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/openrouter/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/openrouter/config.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/openrouter/models.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/openrouter/schema.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/tools/__init__.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/tools/client.py +0 -0
- {mbxai-1.4.0 → mbxai-1.5.0}/src/mbxai/tools/example.py +0 -0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "mbxai"
|
7
|
-
version = "1.
|
7
|
+
version = "1.5.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.
|
85
|
+
"build>=1.5.0.post1",
|
86
86
|
"twine>=6.1.0",
|
87
87
|
]
|
@@ -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,
|
@@ -0,0 +1,220 @@
|
|
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 _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
|
+
|
107
|
+
def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_input_wrapper: bool = False) -> dict[str, Any]:
|
108
|
+
"""Convert a schema to strict format required by OpenAI.
|
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
|
+
|
116
|
+
Args:
|
117
|
+
schema: The input schema to validate and convert
|
118
|
+
strict: Whether to enforce strict validation with additionalProperties: false
|
119
|
+
keep_input_wrapper: Whether to keep the input wrapper (for MCP tools)
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
A schema in strict format with all references resolved
|
123
|
+
"""
|
124
|
+
if not schema:
|
125
|
+
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
126
|
+
|
127
|
+
# Create a new schema object to ensure we have all required fields
|
128
|
+
strict_schema = {
|
129
|
+
"type": "object",
|
130
|
+
"properties": {},
|
131
|
+
"required": [],
|
132
|
+
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
133
|
+
}
|
134
|
+
|
135
|
+
# Store the root schema for $ref resolution
|
136
|
+
root_schema = schema
|
137
|
+
|
138
|
+
# Handle input wrapper
|
139
|
+
if "properties" in schema and "input" in schema["properties"]:
|
140
|
+
inputSchema = schema["properties"]["input"]
|
141
|
+
|
142
|
+
# If input has a $ref, resolve it
|
143
|
+
if "$ref" in inputSchema:
|
144
|
+
inputSchema = _resolve_ref(inputSchema, inputSchema["$ref"], root_schema)
|
145
|
+
|
146
|
+
if keep_input_wrapper:
|
147
|
+
# Create the input property schema
|
148
|
+
input_prop_schema = {
|
149
|
+
"type": "object",
|
150
|
+
"properties": {},
|
151
|
+
"required": [],
|
152
|
+
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
153
|
+
}
|
154
|
+
|
155
|
+
# Process input properties
|
156
|
+
if "properties" in inputSchema:
|
157
|
+
for prop_name, prop in inputSchema["properties"].items():
|
158
|
+
input_prop_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
159
|
+
|
160
|
+
# Copy over required fields for input schema
|
161
|
+
if "required" in inputSchema:
|
162
|
+
input_prop_schema["required"] = inputSchema["required"]
|
163
|
+
|
164
|
+
# Add the input property to the main schema
|
165
|
+
strict_schema["properties"]["input"] = input_prop_schema
|
166
|
+
|
167
|
+
# Copy over required fields for main schema
|
168
|
+
if "required" in schema:
|
169
|
+
strict_schema["required"] = schema["required"]
|
170
|
+
else:
|
171
|
+
# If not keeping input wrapper, use input schema directly
|
172
|
+
if "properties" in inputSchema:
|
173
|
+
for prop_name, prop in inputSchema["properties"].items():
|
174
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
175
|
+
|
176
|
+
# Copy over required fields
|
177
|
+
if "required" in inputSchema:
|
178
|
+
strict_schema["required"] = inputSchema["required"]
|
179
|
+
else:
|
180
|
+
# If no input wrapper, use the schema as is
|
181
|
+
if "properties" in schema:
|
182
|
+
for prop_name, prop in schema["properties"].items():
|
183
|
+
strict_schema["properties"][prop_name] = _convert_property_to_strict(prop, root_schema)
|
184
|
+
|
185
|
+
# Copy over required fields
|
186
|
+
if "required" in schema:
|
187
|
+
strict_schema["required"] = schema["required"]
|
188
|
+
|
189
|
+
return strict_schema
|
190
|
+
|
191
|
+
class ToolCall(BaseModel):
|
192
|
+
"""A tool call from the model."""
|
193
|
+
id: str
|
194
|
+
name: str
|
195
|
+
arguments: dict[str, Any]
|
196
|
+
|
197
|
+
class Tool(BaseModel):
|
198
|
+
"""A tool that can be used by the model."""
|
199
|
+
name: str
|
200
|
+
description: str
|
201
|
+
function: Callable[..., Any] | None = None # Make function optional
|
202
|
+
schema: dict[str, Any]
|
203
|
+
|
204
|
+
def to_openai_function(self) -> dict[str, Any]:
|
205
|
+
"""Convert the tool to an OpenAI function definition."""
|
206
|
+
# Ensure schema is in strict format
|
207
|
+
strict_schema = convert_to_strict_schema(self.schema)
|
208
|
+
|
209
|
+
function_def = {
|
210
|
+
"type": "function",
|
211
|
+
"function": {
|
212
|
+
"name": self.name,
|
213
|
+
"description": self.description,
|
214
|
+
"parameters": strict_schema,
|
215
|
+
"strict": True
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
logger.debug(f"(types) Created function definition for {self.name}: {json.dumps(function_def, indent=2)}")
|
220
|
+
return function_def
|
@@ -0,0 +1,204 @@
|
|
1
|
+
"""
|
2
|
+
Test for schema conversion with Pydantic models containing arrays.
|
3
|
+
|
4
|
+
This test demonstrates the fix for the "array schema missing items" error
|
5
|
+
when converting Pydantic models to OpenAI function schemas.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
from typing import Any
|
10
|
+
from pydantic import BaseModel, Field
|
11
|
+
|
12
|
+
from src.mbxai.tools.types import convert_to_strict_schema
|
13
|
+
|
14
|
+
|
15
|
+
class MetadataFilter(BaseModel):
|
16
|
+
"""Model for a single metadata filter key-value pair.
|
17
|
+
|
18
|
+
Each filter represents one condition to apply to the search.
|
19
|
+
Common filter keys include:
|
20
|
+
- category: Content category (e.g., 'api', 'plugins', 'themes')
|
21
|
+
- version: Shopware version (e.g., '6.5', '6.6')
|
22
|
+
- title: Document title (partial match)
|
23
|
+
- optimized: Whether content was AI-optimized (true/false)
|
24
|
+
- source_url: Source URL (partial match)
|
25
|
+
- optimization_strategy: Strategy used ('enhance_readability', etc.)
|
26
|
+
- ai_generated_metadata: Whether metadata was AI-generated (true/false)
|
27
|
+
"""
|
28
|
+
|
29
|
+
key: str = Field(description="The metadata field name to filter by")
|
30
|
+
value: Any = Field(description="The value to filter for")
|
31
|
+
|
32
|
+
|
33
|
+
class ShopwareKnowledgeSearchInput(BaseModel):
|
34
|
+
"""Input model for Shopware knowledge search."""
|
35
|
+
|
36
|
+
query: str = Field(
|
37
|
+
description="The search query to find relevant Shopware knowledge and documentation"
|
38
|
+
)
|
39
|
+
max_results: int = Field(
|
40
|
+
description="Maximum number of search results to return (1-20)",
|
41
|
+
ge=1,
|
42
|
+
le=20
|
43
|
+
)
|
44
|
+
include_metadata: bool = Field(
|
45
|
+
description="Whether to include metadata in the search results"
|
46
|
+
)
|
47
|
+
metadata_filter: list[MetadataFilter] = Field(
|
48
|
+
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'}]"
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
def test_schema_conversion():
|
53
|
+
"""Test that Pydantic models are properly converted to OpenAI-compatible schemas."""
|
54
|
+
|
55
|
+
print("🧪 Testing Schema Conversion for Shopware Knowledge Search")
|
56
|
+
print("=" * 60)
|
57
|
+
|
58
|
+
# Generate the JSON schema from the Pydantic model
|
59
|
+
print("\n1. Generating Pydantic JSON Schema...")
|
60
|
+
pydantic_schema = ShopwareKnowledgeSearchInput.model_json_schema()
|
61
|
+
|
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
|
+
pydantic_schema,
|
69
|
+
strict=True,
|
70
|
+
keep_input_wrapper=False
|
71
|
+
)
|
72
|
+
|
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
|
+
|
101
|
+
function_def = {
|
102
|
+
"type": "function",
|
103
|
+
"function": {
|
104
|
+
"name": "search_shopware_knowledge",
|
105
|
+
"description": "Search Shopware knowledge base for relevant documentation and information",
|
106
|
+
"parameters": strict_schema_no_wrapper,
|
107
|
+
"strict": True
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
print("OpenAI Function Definition:")
|
112
|
+
print(json.dumps(function_def, indent=2))
|
113
|
+
|
114
|
+
# Validation checks
|
115
|
+
print("\n5. Validation Checks...")
|
116
|
+
print("✅ Checking that all arrays have 'items' property...")
|
117
|
+
|
118
|
+
def check_arrays_have_items(schema, path=""):
|
119
|
+
"""Recursively check that all arrays have items property."""
|
120
|
+
issues = []
|
121
|
+
|
122
|
+
if isinstance(schema, dict):
|
123
|
+
if schema.get("type") == "array":
|
124
|
+
if "items" not in schema:
|
125
|
+
issues.append(f"Array at {path} missing 'items' property")
|
126
|
+
else:
|
127
|
+
print(f" ✓ Array at {path} has items: {schema['items'].get('type', 'unknown')}")
|
128
|
+
# Recursively check items
|
129
|
+
issues.extend(check_arrays_have_items(schema["items"], f"{path}.items"))
|
130
|
+
|
131
|
+
# Check properties
|
132
|
+
if "properties" in schema:
|
133
|
+
for prop_name, prop_schema in schema["properties"].items():
|
134
|
+
prop_path = f"{path}.{prop_name}" if path else prop_name
|
135
|
+
issues.extend(check_arrays_have_items(prop_schema, prop_path))
|
136
|
+
|
137
|
+
return issues
|
138
|
+
|
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")
|
146
|
+
|
147
|
+
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")
|
153
|
+
|
154
|
+
print("\n✅ Checking that all objects have additionalProperties: false...")
|
155
|
+
def check_additional_properties(schema, path=""):
|
156
|
+
"""Recursively check that all objects have additionalProperties: false."""
|
157
|
+
issues = []
|
158
|
+
|
159
|
+
if isinstance(schema, dict):
|
160
|
+
if schema.get("type") == "object":
|
161
|
+
if schema.get("additionalProperties") is not False:
|
162
|
+
issues.append(f"Object at {path} missing 'additionalProperties: false'")
|
163
|
+
else:
|
164
|
+
print(f" ✓ Object at {path} has additionalProperties: false")
|
165
|
+
|
166
|
+
# Check nested schemas
|
167
|
+
for key, value in schema.items():
|
168
|
+
if key in ["properties", "items"] and isinstance(value, dict):
|
169
|
+
if key == "properties":
|
170
|
+
for prop_name, prop_schema in value.items():
|
171
|
+
prop_path = f"{path}.{prop_name}" if path else prop_name
|
172
|
+
issues.extend(check_additional_properties(prop_schema, prop_path))
|
173
|
+
else: # items
|
174
|
+
issues.extend(check_additional_properties(value, f"{path}.items"))
|
175
|
+
|
176
|
+
return issues
|
177
|
+
|
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")
|
185
|
+
|
186
|
+
print("\n🎉 All tests passed! Schema is OpenAI/OpenRouter compatible!")
|
187
|
+
print("\nKey improvements:")
|
188
|
+
print("- ✅ Arrays have proper 'items' definitions")
|
189
|
+
print("- ✅ No $ref or $defs (fully inlined)")
|
190
|
+
print("- ✅ All objects have additionalProperties: false")
|
191
|
+
print("- ✅ Constraints preserved (ge=1, le=20 for max_results)")
|
192
|
+
print("- ✅ Complex nested structures handled correctly")
|
193
|
+
|
194
|
+
return function_def
|
195
|
+
|
196
|
+
|
197
|
+
if __name__ == "__main__":
|
198
|
+
try:
|
199
|
+
function_def = test_schema_conversion()
|
200
|
+
print(f"\n✅ Test completed successfully!")
|
201
|
+
print(f"Function definition ready for OpenRouter API")
|
202
|
+
except Exception as e:
|
203
|
+
print(f"\n❌ Test failed: {e}")
|
204
|
+
raise
|
@@ -1,147 +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
|
-
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, keep_input_wrapper: bool = False) -> 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
|
-
keep_input_wrapper: Whether to keep the input wrapper (for MCP tools)
|
19
|
-
|
20
|
-
Returns:
|
21
|
-
A schema in strict format
|
22
|
-
"""
|
23
|
-
if not schema:
|
24
|
-
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
25
|
-
|
26
|
-
# Create a new schema object to ensure we have all required fields
|
27
|
-
strict_schema = {
|
28
|
-
"type": "object",
|
29
|
-
"properties": {},
|
30
|
-
"required": [],
|
31
|
-
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
32
|
-
}
|
33
|
-
|
34
|
-
# Handle input wrapper
|
35
|
-
if "properties" in schema and "input" in schema["properties"]:
|
36
|
-
inputSchema = schema["properties"]["input"]
|
37
|
-
|
38
|
-
# If input has a $ref, resolve it
|
39
|
-
if "$ref" in inputSchema:
|
40
|
-
ref = inputSchema["$ref"].split("/")[-1]
|
41
|
-
inputSchema = schema.get("$defs", {}).get(ref, {})
|
42
|
-
|
43
|
-
if keep_input_wrapper:
|
44
|
-
# Create the input property schema
|
45
|
-
input_prop_schema = {
|
46
|
-
"type": "object",
|
47
|
-
"properties": {},
|
48
|
-
"required": [],
|
49
|
-
"additionalProperties": False # Always enforce additionalProperties: false for OpenRouter
|
50
|
-
}
|
51
|
-
|
52
|
-
# Copy over input properties
|
53
|
-
if "properties" in inputSchema:
|
54
|
-
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
|
66
|
-
|
67
|
-
# Copy over required fields for input schema
|
68
|
-
if "required" in inputSchema:
|
69
|
-
input_prop_schema["required"] = inputSchema["required"]
|
70
|
-
|
71
|
-
# Add the input property to the main schema
|
72
|
-
strict_schema["properties"]["input"] = input_prop_schema
|
73
|
-
|
74
|
-
# Copy over required fields for main schema
|
75
|
-
if "required" in schema:
|
76
|
-
strict_schema["required"] = schema["required"]
|
77
|
-
else:
|
78
|
-
# If not keeping input wrapper, use input schema directly
|
79
|
-
if "properties" in inputSchema:
|
80
|
-
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
|
92
|
-
|
93
|
-
# Copy over required fields
|
94
|
-
if "required" in inputSchema:
|
95
|
-
strict_schema["required"] = inputSchema["required"]
|
96
|
-
else:
|
97
|
-
# If no input wrapper, use the schema as is
|
98
|
-
if "properties" in schema:
|
99
|
-
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
|
111
|
-
|
112
|
-
# Copy over required fields
|
113
|
-
if "required" in schema:
|
114
|
-
strict_schema["required"] = schema["required"]
|
115
|
-
|
116
|
-
return strict_schema
|
117
|
-
|
118
|
-
class ToolCall(BaseModel):
|
119
|
-
"""A tool call from the model."""
|
120
|
-
id: str
|
121
|
-
name: str
|
122
|
-
arguments: dict[str, Any]
|
123
|
-
|
124
|
-
class Tool(BaseModel):
|
125
|
-
"""A tool that can be used by the model."""
|
126
|
-
name: str
|
127
|
-
description: str
|
128
|
-
function: Callable[..., Any] | None = None # Make function optional
|
129
|
-
schema: dict[str, Any]
|
130
|
-
|
131
|
-
def to_openai_function(self) -> dict[str, Any]:
|
132
|
-
"""Convert the tool to an OpenAI function definition."""
|
133
|
-
# Ensure schema is in strict format
|
134
|
-
strict_schema = convert_to_strict_schema(self.schema)
|
135
|
-
|
136
|
-
function_def = {
|
137
|
-
"type": "function",
|
138
|
-
"function": {
|
139
|
-
"name": self.name,
|
140
|
-
"description": self.description,
|
141
|
-
"parameters": strict_schema,
|
142
|
-
"strict": True
|
143
|
-
}
|
144
|
-
}
|
145
|
-
|
146
|
-
logger.debug(f"(types) Created function definition for {self.name}: {json.dumps(function_def, indent=2)}")
|
147
|
-
return function_def
|
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
|
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
|
File without changes
|
File without changes
|