mbxai 1.5.0__py3-none-any.whl → 2.0.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 +18 -1
- mbxai/agent/__init__.py +8 -0
- mbxai/agent/client.py +450 -0
- mbxai/agent/models.py +56 -0
- mbxai/examples/agent_example.py +152 -0
- mbxai/examples/agent_iterations_example.py +173 -0
- mbxai/examples/agent_tool_registration_example.py +247 -0
- mbxai/examples/agent_validation_example.py +123 -0
- mbxai/examples/auto_schema_example.py +228 -0
- mbxai/examples/simple_agent_test.py +168 -0
- mbxai/mcp/server.py +1 -1
- mbxai/tools/client.py +57 -5
- mbxai/tools/types.py +32 -7
- mbxai-2.0.0.dist-info/METADATA +346 -0
- {mbxai-1.5.0.dist-info → mbxai-2.0.0.dist-info}/RECORD +17 -8
- mbxai-1.5.0.dist-info/METADATA +0 -169
- {mbxai-1.5.0.dist-info → mbxai-2.0.0.dist-info}/WHEEL +0 -0
- {mbxai-1.5.0.dist-info → mbxai-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,228 @@
|
|
1
|
+
"""
|
2
|
+
Example demonstrating automatic schema generation from function signatures.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
from typing import Optional
|
7
|
+
from pydantic import BaseModel, Field
|
8
|
+
from mbxai import AgentClient, ToolClient, OpenRouterClient
|
9
|
+
|
10
|
+
|
11
|
+
class WeatherResponse(BaseModel):
|
12
|
+
"""Response for weather queries."""
|
13
|
+
location: str = Field(description="The location")
|
14
|
+
temperature: str = Field(description="Current temperature")
|
15
|
+
conditions: str = Field(description="Weather conditions")
|
16
|
+
recommendations: list[str] = Field(description="Recommendations based on weather")
|
17
|
+
|
18
|
+
|
19
|
+
def get_weather(location: str, unit: str = "fahrenheit") -> dict:
|
20
|
+
"""Get weather information for a location.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
location: The city or location name
|
24
|
+
unit: Temperature unit (fahrenheit or celsius)
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Weather information dictionary
|
28
|
+
"""
|
29
|
+
# Mock weather service
|
30
|
+
temp = "72°F" if unit == "fahrenheit" else "22°C"
|
31
|
+
return {
|
32
|
+
"location": location,
|
33
|
+
"temperature": temp,
|
34
|
+
"conditions": "Sunny"
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
def calculate_tip(bill_amount: float, tip_percentage: float = 18.0, split_count: int = 1) -> dict:
|
39
|
+
"""Calculate tip and split the bill.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
bill_amount: Total bill amount in dollars
|
43
|
+
tip_percentage: Tip percentage (default 18%)
|
44
|
+
split_count: Number of people to split between (default 1)
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Calculation results
|
48
|
+
"""
|
49
|
+
tip_amount = bill_amount * (tip_percentage / 100)
|
50
|
+
total_amount = bill_amount + tip_amount
|
51
|
+
per_person = total_amount / split_count
|
52
|
+
|
53
|
+
return {
|
54
|
+
"bill_amount": bill_amount,
|
55
|
+
"tip_amount": tip_amount,
|
56
|
+
"total_amount": total_amount,
|
57
|
+
"per_person": per_person,
|
58
|
+
"split_count": split_count
|
59
|
+
}
|
60
|
+
|
61
|
+
|
62
|
+
def search_knowledge(query: str, category: Optional[str] = None, max_results: int = 5) -> dict:
|
63
|
+
"""Search knowledge base for information.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
query: Search query string
|
67
|
+
category: Optional category filter
|
68
|
+
max_results: Maximum number of results to return
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Search results
|
72
|
+
"""
|
73
|
+
# Mock search results
|
74
|
+
return {
|
75
|
+
"query": query,
|
76
|
+
"category": category,
|
77
|
+
"results": [f"Result {i+1} for '{query}'" for i in range(min(max_results, 3))],
|
78
|
+
"total_found": max_results
|
79
|
+
}
|
80
|
+
|
81
|
+
|
82
|
+
def demo_automatic_schema_generation():
|
83
|
+
"""Demonstrate automatic schema generation from function signatures."""
|
84
|
+
|
85
|
+
print("=== Automatic Schema Generation Example ===\n")
|
86
|
+
|
87
|
+
# Initialize clients
|
88
|
+
openrouter_client = OpenRouterClient(token=os.getenv("OPENROUTER_API_KEY", "test-token"))
|
89
|
+
tool_client = ToolClient(openrouter_client)
|
90
|
+
agent = AgentClient(tool_client)
|
91
|
+
|
92
|
+
print("Registering tools with automatic schema generation...\n")
|
93
|
+
|
94
|
+
# Register tools WITHOUT providing schemas - they'll be auto-generated
|
95
|
+
tools_to_register = [
|
96
|
+
{
|
97
|
+
"name": "get_weather",
|
98
|
+
"description": "Get current weather for a location",
|
99
|
+
"function": get_weather
|
100
|
+
},
|
101
|
+
{
|
102
|
+
"name": "calculate_tip",
|
103
|
+
"description": "Calculate tip and split bill between people",
|
104
|
+
"function": calculate_tip
|
105
|
+
},
|
106
|
+
{
|
107
|
+
"name": "search_knowledge",
|
108
|
+
"description": "Search knowledge base for information",
|
109
|
+
"function": search_knowledge
|
110
|
+
}
|
111
|
+
]
|
112
|
+
|
113
|
+
for tool_info in tools_to_register:
|
114
|
+
try:
|
115
|
+
# Register without schema - will be auto-generated
|
116
|
+
agent.register_tool(
|
117
|
+
name=tool_info["name"],
|
118
|
+
description=tool_info["description"],
|
119
|
+
function=tool_info["function"]
|
120
|
+
# Note: No schema parameter - it will be auto-generated!
|
121
|
+
)
|
122
|
+
print(f"✅ Registered '{tool_info['name']}' with auto-generated schema")
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
print(f"❌ Failed to register '{tool_info['name']}': {e}")
|
126
|
+
|
127
|
+
print("\n" + "="*50 + "\n")
|
128
|
+
|
129
|
+
# Show what schemas were generated
|
130
|
+
print("Generated Tool Schemas:")
|
131
|
+
print("-" * 25)
|
132
|
+
|
133
|
+
# Access the registered tools to show their schemas
|
134
|
+
if hasattr(tool_client, '_tools'):
|
135
|
+
for tool_name, tool in tool_client._tools.items():
|
136
|
+
print(f"\n🔧 Tool: {tool_name}")
|
137
|
+
print(f" Description: {tool.description}")
|
138
|
+
print(f" Schema keys: {list(tool.schema.get('properties', {}).keys())}")
|
139
|
+
print(f" Required params: {tool.schema.get('required', [])}")
|
140
|
+
|
141
|
+
# Show a few key properties
|
142
|
+
properties = tool.schema.get('properties', {})
|
143
|
+
for prop_name, prop_schema in list(properties.items())[:3]: # Show first 3
|
144
|
+
prop_type = prop_schema.get('type', 'unknown')
|
145
|
+
prop_desc = prop_schema.get('description', 'No description')
|
146
|
+
print(f" - {prop_name}: {prop_type} - {prop_desc}")
|
147
|
+
|
148
|
+
print("\n" + "="*50 + "\n")
|
149
|
+
|
150
|
+
# Demonstrate usage examples
|
151
|
+
print("Example usage with auto-generated schemas:")
|
152
|
+
print('agent.agent("What\'s the weather in Tokyo?", WeatherResponse)')
|
153
|
+
print('agent.agent("Calculate tip for a $85 bill split 4 ways", CalculationResponse)')
|
154
|
+
print('agent.agent("Search for Python tutorials", SearchResponse)')
|
155
|
+
|
156
|
+
|
157
|
+
def demo_manual_vs_automatic():
|
158
|
+
"""Compare manual schema vs automatic generation."""
|
159
|
+
|
160
|
+
print("\n=== Manual vs Automatic Schema Comparison ===\n")
|
161
|
+
|
162
|
+
openrouter_client = OpenRouterClient(token="test-token")
|
163
|
+
tool_client = ToolClient(openrouter_client)
|
164
|
+
|
165
|
+
# Example function
|
166
|
+
def example_function(name: str, age: int, active: bool = True) -> str:
|
167
|
+
"""Example function for schema comparison.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
name: Person's name
|
171
|
+
age: Person's age in years
|
172
|
+
active: Whether person is active
|
173
|
+
"""
|
174
|
+
return f"{name} is {age} years old and {'active' if active else 'inactive'}"
|
175
|
+
|
176
|
+
print("1. Manual Schema (traditional approach):")
|
177
|
+
manual_schema = {
|
178
|
+
"type": "object",
|
179
|
+
"properties": {
|
180
|
+
"name": {"type": "string", "description": "Person's name"},
|
181
|
+
"age": {"type": "integer", "description": "Person's age in years"},
|
182
|
+
"active": {"type": "boolean", "description": "Whether person is active"}
|
183
|
+
},
|
184
|
+
"required": ["name", "age"],
|
185
|
+
"additionalProperties": False
|
186
|
+
}
|
187
|
+
print(f" Properties: {list(manual_schema['properties'].keys())}")
|
188
|
+
print(f" Required: {manual_schema['required']}")
|
189
|
+
|
190
|
+
print("\n2. Automatic Schema (from function signature):")
|
191
|
+
try:
|
192
|
+
# Register with auto-generation
|
193
|
+
tool_client.register_tool(
|
194
|
+
"example_tool",
|
195
|
+
"Example tool with auto-generated schema",
|
196
|
+
example_function
|
197
|
+
# No schema provided - will be auto-generated
|
198
|
+
)
|
199
|
+
|
200
|
+
auto_tool = tool_client._tools.get("example_tool")
|
201
|
+
if auto_tool:
|
202
|
+
auto_schema = auto_tool.schema
|
203
|
+
print(f" Properties: {list(auto_schema.get('properties', {}).keys())}")
|
204
|
+
print(f" Required: {auto_schema.get('required', [])}")
|
205
|
+
print(f" Additional Properties: {auto_schema.get('additionalProperties')}")
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
print(f" Error: {e}")
|
209
|
+
|
210
|
+
print("\n✨ Benefits of automatic generation:")
|
211
|
+
print(" - No manual schema writing required")
|
212
|
+
print(" - Automatically extracts types from function signature")
|
213
|
+
print(" - Handles default values correctly")
|
214
|
+
print(" - Uses existing convert_to_strict_schema pipeline")
|
215
|
+
print(" - Reduces chance of schema/function mismatch")
|
216
|
+
|
217
|
+
|
218
|
+
if __name__ == "__main__":
|
219
|
+
demo_automatic_schema_generation()
|
220
|
+
demo_manual_vs_automatic()
|
221
|
+
|
222
|
+
print("\n" + "="*60)
|
223
|
+
print("Automatic schema generation demo completed! 🎉")
|
224
|
+
print("\nKey benefits:")
|
225
|
+
print("• Zero-config tool registration")
|
226
|
+
print("• Type-safe schema generation")
|
227
|
+
print("• Leverages existing Pydantic infrastructure")
|
228
|
+
print("• Compatible with MCPTool.to_openai_function() approach")
|
@@ -0,0 +1,168 @@
|
|
1
|
+
"""
|
2
|
+
Simple test to verify AgentClient functionality.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
from mbxai.agent import AgentClient, AgentResponse, Question, Result
|
7
|
+
from mbxai.openrouter import OpenRouterClient
|
8
|
+
|
9
|
+
|
10
|
+
class SimpleResponse(BaseModel):
|
11
|
+
"""A simple response structure for testing."""
|
12
|
+
message: str = Field(description="The response message")
|
13
|
+
confidence: float = Field(description="Confidence level from 0.0 to 1.0")
|
14
|
+
|
15
|
+
|
16
|
+
def test_agent_validation():
|
17
|
+
"""Test AgentClient validation of parse method requirement."""
|
18
|
+
|
19
|
+
print("=== Testing AgentClient Validation ===")
|
20
|
+
|
21
|
+
# Test 1: Valid client with parse method (OpenRouterClient-like)
|
22
|
+
class MockOpenRouterClient:
|
23
|
+
def parse(self, messages, response_format):
|
24
|
+
# Mock response based on the format
|
25
|
+
class MockChoice:
|
26
|
+
def __init__(self, content):
|
27
|
+
self.message = MockMessage(content)
|
28
|
+
|
29
|
+
class MockMessage:
|
30
|
+
def __init__(self, content):
|
31
|
+
self.content = content
|
32
|
+
self.parsed = None
|
33
|
+
|
34
|
+
class MockResponse:
|
35
|
+
def __init__(self, content):
|
36
|
+
self.choices = [MockChoice(content)]
|
37
|
+
|
38
|
+
# Return appropriate mock response based on format
|
39
|
+
if response_format.__name__ == "QuestionList":
|
40
|
+
return MockResponse('{"questions": []}')
|
41
|
+
elif response_format.__name__ == "Result":
|
42
|
+
return MockResponse('{"result": "This is a test result"}')
|
43
|
+
elif response_format.__name__ == "QualityCheck":
|
44
|
+
return MockResponse('{"is_good": true, "feedback": ""}')
|
45
|
+
elif response_format.__name__ == "SimpleResponse":
|
46
|
+
return MockResponse('{"message": "Test completed successfully", "confidence": 0.95}')
|
47
|
+
else:
|
48
|
+
return MockResponse('{"result": "Default response"}')
|
49
|
+
|
50
|
+
try:
|
51
|
+
mock_client = MockOpenRouterClient()
|
52
|
+
agent = AgentClient(mock_client)
|
53
|
+
print("✅ Valid client accepted - has parse method")
|
54
|
+
|
55
|
+
# Test max_iterations parameter
|
56
|
+
agent_custom = AgentClient(mock_client, max_iterations=3)
|
57
|
+
print("✅ Custom max_iterations parameter accepted")
|
58
|
+
|
59
|
+
# Test tool registration (should fail for OpenRouterClient-like)
|
60
|
+
try:
|
61
|
+
agent.register_tool("test", "desc", lambda: None, {})
|
62
|
+
print("❌ Tool registration should have failed for OpenRouterClient-like")
|
63
|
+
except AttributeError:
|
64
|
+
print("✅ Tool registration correctly rejected for OpenRouterClient-like")
|
65
|
+
|
66
|
+
except ValueError as e:
|
67
|
+
print(f"❌ Valid client rejected: {e}")
|
68
|
+
return False
|
69
|
+
|
70
|
+
# Test 2: Valid client with parse and register_tool (ToolClient-like)
|
71
|
+
class MockToolClient:
|
72
|
+
def parse(self, messages, response_format):
|
73
|
+
return MockOpenRouterClient().parse(messages, response_format)
|
74
|
+
|
75
|
+
def register_tool(self, name, description, function, schema):
|
76
|
+
# Mock tool registration
|
77
|
+
pass
|
78
|
+
|
79
|
+
try:
|
80
|
+
mock_tool_client = MockToolClient()
|
81
|
+
agent = AgentClient(mock_tool_client)
|
82
|
+
print("✅ ToolClient-like client accepted")
|
83
|
+
|
84
|
+
# Test tool registration (should succeed)
|
85
|
+
agent.register_tool("test", "desc", lambda: None, {})
|
86
|
+
print("✅ Tool registration succeeded for ToolClient-like")
|
87
|
+
|
88
|
+
except Exception as e:
|
89
|
+
print(f"❌ ToolClient-like test failed: {e}")
|
90
|
+
|
91
|
+
# Test 3: Invalid client without parse method
|
92
|
+
class MockInvalidClient:
|
93
|
+
def create(self, messages, **kwargs):
|
94
|
+
return "No parse method"
|
95
|
+
|
96
|
+
try:
|
97
|
+
invalid_client = MockInvalidClient()
|
98
|
+
agent = AgentClient(invalid_client)
|
99
|
+
print("❌ Invalid client accepted - should have been rejected!")
|
100
|
+
return False
|
101
|
+
except ValueError as e:
|
102
|
+
print(f"✅ Invalid client correctly rejected: {e}")
|
103
|
+
|
104
|
+
return True
|
105
|
+
|
106
|
+
|
107
|
+
def test_agent_workflow():
|
108
|
+
"""Test the basic agent workflow without actual API calls."""
|
109
|
+
|
110
|
+
# Mock OpenRouter client for testing
|
111
|
+
class MockOpenRouterClient:
|
112
|
+
def parse(self, messages, response_format):
|
113
|
+
# Mock response based on the format
|
114
|
+
class MockChoice:
|
115
|
+
def __init__(self, content):
|
116
|
+
self.message = MockMessage(content)
|
117
|
+
|
118
|
+
class MockMessage:
|
119
|
+
def __init__(self, content):
|
120
|
+
self.content = content
|
121
|
+
self.parsed = None
|
122
|
+
|
123
|
+
class MockResponse:
|
124
|
+
def __init__(self, content):
|
125
|
+
self.choices = [MockChoice(content)]
|
126
|
+
|
127
|
+
# Return appropriate mock response based on format
|
128
|
+
if response_format.__name__ == "QuestionList":
|
129
|
+
return MockResponse('{"questions": []}')
|
130
|
+
elif response_format.__name__ == "Result":
|
131
|
+
return MockResponse('{"result": "This is a test result"}')
|
132
|
+
elif response_format.__name__ == "QualityCheck":
|
133
|
+
return MockResponse('{"is_good": true, "feedback": ""}')
|
134
|
+
elif response_format.__name__ == "SimpleResponse":
|
135
|
+
return MockResponse('{"message": "Test completed successfully", "confidence": 0.95}')
|
136
|
+
else:
|
137
|
+
return MockResponse('{"result": "Default response"}')
|
138
|
+
|
139
|
+
print("\n=== Testing Agent Workflow ===")
|
140
|
+
|
141
|
+
# Test the agent
|
142
|
+
mock_client = MockOpenRouterClient()
|
143
|
+
agent = AgentClient(mock_client)
|
144
|
+
|
145
|
+
# Test without questions
|
146
|
+
prompt = "Test prompt"
|
147
|
+
response = agent.agent(prompt, SimpleResponse, ask_questions=False)
|
148
|
+
|
149
|
+
print("Agent Test Results:")
|
150
|
+
print(f"Has questions: {response.has_questions()}")
|
151
|
+
print(f"Is complete: {response.is_complete()}")
|
152
|
+
|
153
|
+
if response.is_complete():
|
154
|
+
print(f"Final response type: {type(response.final_response)}")
|
155
|
+
print(f"Final response: {response.final_response}")
|
156
|
+
|
157
|
+
return response
|
158
|
+
|
159
|
+
|
160
|
+
if __name__ == "__main__":
|
161
|
+
# Run validation tests first
|
162
|
+
validation_passed = test_agent_validation()
|
163
|
+
|
164
|
+
if validation_passed:
|
165
|
+
# Then run workflow test
|
166
|
+
test_agent_workflow()
|
167
|
+
else:
|
168
|
+
print("❌ Validation tests failed, skipping workflow test")
|
mbxai/mcp/server.py
CHANGED
mbxai/tools/client.py
CHANGED
@@ -6,14 +6,57 @@ from typing import Any, Callable, TypeVar
|
|
6
6
|
import logging
|
7
7
|
import inspect
|
8
8
|
import json
|
9
|
-
from pydantic import BaseModel
|
9
|
+
from pydantic import BaseModel, create_model
|
10
10
|
from ..openrouter import OpenRouterClient
|
11
|
-
from .types import Tool
|
11
|
+
from .types import Tool, convert_to_strict_schema
|
12
12
|
|
13
13
|
logger = logging.getLogger(__name__)
|
14
14
|
|
15
15
|
T = TypeVar("T", bound=BaseModel)
|
16
16
|
|
17
|
+
|
18
|
+
def _generate_schema_from_function(func: Callable[..., Any]) -> dict[str, Any]:
|
19
|
+
"""Generate JSON schema from function signature using Pydantic's model_json_schema."""
|
20
|
+
try:
|
21
|
+
sig = inspect.signature(func)
|
22
|
+
|
23
|
+
# Extract fields for the Pydantic model
|
24
|
+
fields = {}
|
25
|
+
|
26
|
+
for param_name, param in sig.parameters.items():
|
27
|
+
# Skip self parameter
|
28
|
+
if param_name == 'self':
|
29
|
+
continue
|
30
|
+
|
31
|
+
# Get the parameter type annotation
|
32
|
+
param_type = param.annotation if param.annotation != inspect.Parameter.empty else str
|
33
|
+
|
34
|
+
# Handle default values
|
35
|
+
if param.default != inspect.Parameter.empty:
|
36
|
+
fields[param_name] = (param_type, param.default)
|
37
|
+
else:
|
38
|
+
fields[param_name] = (param_type, ...) # Required field
|
39
|
+
|
40
|
+
# Create a temporary Pydantic model
|
41
|
+
temp_model = create_model('TempToolModel', **fields)
|
42
|
+
|
43
|
+
# Generate schema using Pydantic's built-in method
|
44
|
+
schema = temp_model.model_json_schema()
|
45
|
+
|
46
|
+
logger.debug(f"Generated schema for function {func.__name__}: {json.dumps(schema, indent=2)}")
|
47
|
+
return schema
|
48
|
+
|
49
|
+
except Exception as e:
|
50
|
+
logger.warning(f"Failed to generate schema for function {func.__name__}: {e}")
|
51
|
+
# Return a basic schema as fallback
|
52
|
+
return {
|
53
|
+
"type": "object",
|
54
|
+
"properties": {},
|
55
|
+
"required": [],
|
56
|
+
"additionalProperties": False
|
57
|
+
}
|
58
|
+
|
59
|
+
|
17
60
|
class ToolClient:
|
18
61
|
"""Client for handling tool calls with OpenRouter."""
|
19
62
|
|
@@ -31,7 +74,7 @@ class ToolClient:
|
|
31
74
|
name: str,
|
32
75
|
description: str,
|
33
76
|
function: Callable[..., Any],
|
34
|
-
schema: dict[str, Any],
|
77
|
+
schema: dict[str, Any] | None = None,
|
35
78
|
) -> None:
|
36
79
|
"""Register a new tool.
|
37
80
|
|
@@ -39,8 +82,17 @@ class ToolClient:
|
|
39
82
|
name: The name of the tool
|
40
83
|
description: A description of what the tool does
|
41
84
|
function: The function to call when the tool is used
|
42
|
-
schema: The JSON schema for the tool's parameters
|
85
|
+
schema: The JSON schema for the tool's parameters. If None or empty,
|
86
|
+
will be automatically generated from the function signature.
|
43
87
|
"""
|
88
|
+
# Generate schema automatically if not provided or empty
|
89
|
+
if not schema:
|
90
|
+
logger.debug(f"No schema provided for tool '{name}', generating from function signature")
|
91
|
+
raw_schema = _generate_schema_from_function(function)
|
92
|
+
# Use the existing convert_to_strict_schema like MCPTool does
|
93
|
+
schema = convert_to_strict_schema(raw_schema, strict=True, keep_input_wrapper=False)
|
94
|
+
logger.debug(f"Auto-generated and converted schema for '{name}': {json.dumps(schema, indent=2)}")
|
95
|
+
|
44
96
|
tool = Tool(
|
45
97
|
name=name,
|
46
98
|
description=description,
|
@@ -336,7 +388,7 @@ class ToolClient:
|
|
336
388
|
raise ValueError(f"Invalid tool arguments format: {tool_call.function.arguments}")
|
337
389
|
|
338
390
|
# Call the tool
|
339
|
-
logger.
|
391
|
+
logger.info(f"Calling tool: {tool.name} with args: {self._truncate_dict(arguments)}")
|
340
392
|
try:
|
341
393
|
result = tool.function(**arguments)
|
342
394
|
logger.debug(f"Tool {tool.name} completed successfully")
|
mbxai/tools/types.py
CHANGED
@@ -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
|
-
|
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
|
-
|
54
|
-
|
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
|