mbxai 1.6.0__py3-none-any.whl → 2.0.1__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.
@@ -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
@@ -31,7 +31,7 @@ class MCPServer:
31
31
  self.app = FastAPI(
32
32
  title=self.name,
33
33
  description=self.description,
34
- version="1.6.0",
34
+ version="2.0.1",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
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.debug(f"Calling tool: {tool.name} with args: {self._truncate_dict(arguments)}")
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")