mbxai 1.0.13__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mbxai/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "1.0.13"
5
+ __version__ = "1.2.0"
@@ -0,0 +1,127 @@
1
+ {
2
+ "model": "openai/gpt-4.1",
3
+ "messages": [
4
+ {
5
+ "role": "system",
6
+ "content": ""
7
+ },
8
+ {
9
+ "role": "user",
10
+ "content": ""
11
+ }
12
+ ],
13
+ "tools": [
14
+ {
15
+ "type": "function",
16
+ "function": {
17
+ "name": "analyze_html",
18
+ "description": "Analyze HTML content to understand its structure and extract key information.\n\n This function performs a comprehensive analysis of HTML content using AI to:\n - Identify the page structure and layout\n - Extract key information in a key=value format\n - Explain the code organization and hierarchy\n - Identify main content areas and their purposes\n\n Args:\n input (AnalyzeHtmlInput): The input containing HTML code to analyze\n\n Returns:\n str: A structured analysis containing key information and page structure details\n ",
19
+ "parameters": {
20
+ "type": "object",
21
+ "properties": {
22
+ "input": {
23
+ "type": "object",
24
+ "properties": {
25
+ "html": {
26
+ "type": "string",
27
+ "description": "The HTML code to be analyzed"
28
+ }
29
+ },
30
+ "required": ["html"],
31
+ "additionalProperties": false
32
+ }
33
+ },
34
+ "required": ["input"],
35
+ "additionalProperties": false
36
+ },
37
+ "strict": true
38
+ }
39
+ },
40
+ {
41
+ "type": "function",
42
+ "function": {
43
+ "name": "analyze_variant_handling",
44
+ "description": "Analyze an e-commerce product detail page to understand variant handling and selection logic.\n\n This function performs a comprehensive analysis of product variant handling by:\n - Identifying variant selection UI elements\n - Understanding variant types and options\n - Analyzing the variant selection logic\n - Detecting price updates and availability changes\n - Identifying variant-related JavaScript functions\n\n Args:\n input (AnalyzeVariantHandlingInput): The input containing HTML code of the product detail page\n\n Returns:\n str: A structured analysis containing variant handling details\n ",
45
+ "parameters": {
46
+ "type": "object",
47
+ "properties": {
48
+ "input": {
49
+ "type": "object",
50
+ "properties": {
51
+ "html": {
52
+ "type": "string",
53
+ "description": "The HTML code to analyze for variant handling"
54
+ }
55
+ },
56
+ "required": ["html"],
57
+ "additionalProperties": false
58
+ }
59
+ },
60
+ "required": ["input"],
61
+ "additionalProperties": false
62
+ },
63
+ "strict": true
64
+ }
65
+ },
66
+ {
67
+ "type": "function",
68
+ "function": {
69
+ "name": "generate_product_selectors",
70
+ "description": "Generate clear selectors for extracting product information from an HTML page.\n\n This function takes the HTML and analysis results from both analysis tools to generate\n precise selectors that can be used to extract various product information using Python.\n\n Args:\n input (GenerateProductSelectorsInput): The input containing HTML and analysis results\n\n Returns:\n str: A structured set of selectors for extracting product information\n ",
71
+ "parameters": {
72
+ "type": "object",
73
+ "properties": {
74
+ "input": {
75
+ "type": "object",
76
+ "properties": {
77
+ "html": {
78
+ "type": "string",
79
+ "description": "The HTML code to generate selectors for"
80
+ },
81
+ "structure_analysis": {
82
+ "type": "string",
83
+ "description": "The results from analyze_html"
84
+ },
85
+ "variant_analysis": {
86
+ "type": "string",
87
+ "description": "The results from analyze_variant_handling"
88
+ }
89
+ },
90
+ "required": ["html", "structure_analysis", "variant_analysis"],
91
+ "additionalProperties": false
92
+ }
93
+ },
94
+ "required": ["input"],
95
+ "additionalProperties": false
96
+ },
97
+ "strict": true
98
+ }
99
+ },
100
+ {
101
+ "type": "function",
102
+ "function": {
103
+ "name": "scrape_html",
104
+ "description": "Scrape HTML content from a URL.\n\n This function fetches the HTML content from a given URL using httpx.\n It handles redirects and raises appropriate exceptions for HTTP errors.\n\n Args:\n input (ScrapeHtmlInput): The input containing the URL to scrape\n\n Returns:\n ScrapeHtmlOutput: The HTML content of the page\n\n Raises:\n httpx.HTTPError: If there's an HTTP error while fetching the page\n Exception: For any other unexpected errors\n ",
105
+ "parameters": {
106
+ "type": "object",
107
+ "properties": {
108
+ "input": {
109
+ "type": "object",
110
+ "properties": {
111
+ "url": {
112
+ "type": "string",
113
+ "description": "The URL to scrape HTML from"
114
+ }
115
+ },
116
+ "required": ["url"],
117
+ "additionalProperties": false
118
+ }
119
+ },
120
+ "required": ["input"],
121
+ "additionalProperties": false
122
+ },
123
+ "strict": true
124
+ }
125
+ }
126
+ ]
127
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "gen-1746645035-Q9k3fnnriVxdePheqbuo",
3
+ "created": 1746645035,
4
+ "model": "openai/gpt-4.1",
5
+ "choices": [
6
+ {
7
+ "index": 0,
8
+ "message": {
9
+ "role": "assistant",
10
+ "content": "",
11
+ "tool_calls": [
12
+ {
13
+ "id": "call_rMZXIopQAIhToRa5o9nslOnC",
14
+ "type": "function",
15
+ "function": {
16
+ "name": "analyze_variant_handling",
17
+ "arguments": "{\"input\": {\"html\": \"<!DOCTYPE html>\\n\\n<html lang=\\\"de-DE\\\"\\n itemscope=\\\"itemscope\\\"\\n itemtype=\\\"https://schema.org/WebPage\\\">\\n\\n \\n \\n <head>\\n ...\"}}"
18
+ }
19
+ }
20
+ ]
21
+ },
22
+ "finish_reason": "tool_calls"
23
+ }
24
+ ],
25
+ "usage": {
26
+ "prompt_tokens": 44250,
27
+ "completion_tokens": 145,
28
+ "total_tokens": 44395
29
+ }
30
+ }
@@ -0,0 +1,181 @@
1
+ """
2
+ Script to send a request to AI using OpenRouterClient by reading request.json.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import logging
8
+ from pathlib import Path
9
+ from mbxai.openrouter.client import OpenRouterClient
10
+ from pydantic import BaseModel, Field
11
+
12
+ # Configure logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class PageStructureValidator(BaseModel):
21
+ """Model representing a validator to check if an HTML page matches a known structure."""
22
+
23
+ name: str = Field(description="Name of the validator")
24
+ xpath_selector: str = Field(
25
+ description="XPath selector to check if the structure matches"
26
+ )
27
+ description: str = Field(
28
+ description="Description of what this validator checks for"
29
+ )
30
+
31
+
32
+ class ProductSelector(BaseModel):
33
+ """Model representing a selector for extracting specific product data."""
34
+
35
+ field_name: str = Field(description="Name of the product field to extract")
36
+ xpath_selector: str = Field(description="XPath selector to extract the data")
37
+ description: str = Field(description="Description of what this selector extracts")
38
+ is_list: bool = Field(description="Whether this selector should extract a list of values or a single value")
39
+
40
+
41
+ class PageStructureAnalysis(BaseModel):
42
+ """Model representing the analysis of a product page structure."""
43
+
44
+ structure_id: str = Field(description="Unique identifier for this page structure")
45
+ store_name: str = Field(description="Name of the store/website")
46
+ structure_description: str = Field(description="Description of the page structure")
47
+ validators: list[PageStructureValidator] = Field(
48
+ description="Validators to check if HTML matches this structure"
49
+ )
50
+ selectors: list[ProductSelector] = Field(
51
+ description="Selectors to extract product data"
52
+ )
53
+
54
+
55
+ def read_request_json() -> dict:
56
+ """Read and parse the request.json file.
57
+
58
+ Returns:
59
+ dict: The parsed JSON data
60
+ """
61
+ current_dir = Path(__file__).parent
62
+ request_file = current_dir / "request.json"
63
+
64
+ try:
65
+ with open(request_file, 'r', encoding='utf-8') as f:
66
+ return json.load(f)
67
+ except FileNotFoundError:
68
+ logger.error(f"request.json not found at {request_file}")
69
+ raise
70
+ except json.JSONDecodeError as e:
71
+ logger.error(f"Error parsing request.json: {e}")
72
+ raise
73
+
74
+ def format_response(response) -> dict:
75
+ """Format the ChatCompletion response into a JSON-serializable dictionary.
76
+
77
+ Args:
78
+ response: The ChatCompletion response from OpenAI
79
+
80
+ Returns:
81
+ dict: A JSON-serializable dictionary containing the response data
82
+ """
83
+ # Format choices with tool calls if present
84
+ choices = []
85
+ for choice in response.choices:
86
+ choice_data = {
87
+ 'index': choice.index,
88
+ 'message': {
89
+ 'role': choice.message.role,
90
+ 'content': choice.message.content
91
+ },
92
+ 'finish_reason': choice.finish_reason
93
+ }
94
+
95
+ # Add tool calls if they exist
96
+ if hasattr(choice.message, 'tool_calls') and choice.message.tool_calls:
97
+ choice_data['message']['tool_calls'] = [
98
+ {
99
+ 'id': tool_call.id,
100
+ 'type': tool_call.type,
101
+ 'function': {
102
+ 'name': tool_call.function.name,
103
+ 'arguments': tool_call.function.arguments
104
+ }
105
+ }
106
+ for tool_call in choice.message.tool_calls
107
+ ]
108
+
109
+ choices.append(choice_data)
110
+
111
+ return {
112
+ 'id': response.id,
113
+ 'created': response.created,
114
+ 'model': response.model,
115
+ 'choices': choices,
116
+ 'usage': {
117
+ 'prompt_tokens': response.usage.prompt_tokens,
118
+ 'completion_tokens': response.usage.completion_tokens,
119
+ 'total_tokens': response.usage.total_tokens
120
+ }
121
+ }
122
+
123
+ def write_response_json(response_data: dict):
124
+ """Write the response data to response.json file.
125
+
126
+ Args:
127
+ response_data: The formatted response dictionary to write
128
+ """
129
+ current_dir = Path(__file__).parent
130
+ response_file = current_dir / "response.json"
131
+
132
+ try:
133
+ with open(response_file, 'w', encoding='utf-8') as f:
134
+ json.dump(response_data, f, indent=2)
135
+ logger.info(f"Response written to {response_file}")
136
+ except IOError as e:
137
+ logger.error(f"Error writing response.json: {e}")
138
+ raise
139
+
140
+ def main():
141
+ # Get API token from environment variable
142
+ token = os.getenv("OPENROUTER_API_KEY")
143
+ if not token:
144
+ logger.error("OPENROUTER_API_KEY environment variable not set")
145
+ raise ValueError("Please set the OPENROUTER_API_KEY environment variable")
146
+
147
+ # Read request configuration
148
+ logger.info("Reading request.json")
149
+ request_data = read_request_json()
150
+
151
+ # Initialize the OpenRouter client
152
+ logger.info("Initializing OpenRouterClient")
153
+ client = OpenRouterClient(token=token)
154
+
155
+ # Extract request parameters
156
+ model = request_data.get("model")
157
+ messages = request_data.get("messages", [])
158
+ tools = request_data.get("tools", [])
159
+
160
+ # Send the request
161
+ logger.info(f"Sending request to model: {model}")
162
+ response = client.parse(
163
+ model=model,
164
+ response_format=PageStructureAnalysis,
165
+ messages=messages,
166
+ tools=tools
167
+ )
168
+
169
+ # Format and save the response
170
+ logger.info("Received response from OpenRouter API")
171
+ formatted_response = format_response(response)
172
+ write_response_json(formatted_response)
173
+
174
+ # Print summary
175
+ print("\nResponse summary:")
176
+ print(f"- Model: {formatted_response['model']}")
177
+ print(f"- Total tokens: {formatted_response['usage']['total_tokens']}")
178
+ print(f"- Response saved to: {Path(__file__).parent / 'response.json'}")
179
+
180
+ if __name__ == "__main__":
181
+ main()
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.0.13",
34
+ version="1.2.0",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -2,10 +2,13 @@
2
2
  OpenRouter client implementation.
3
3
  """
4
4
 
5
- from typing import Any, Optional, Union
5
+ from typing import Any, Optional, Union, Type
6
6
  from openai import OpenAI, OpenAIError, RateLimitError, APITimeoutError, APIConnectionError, BadRequestError, AuthenticationError
7
+ from openai.lib._parsing import type_to_response_format_param
7
8
  from .models import OpenRouterModel, OpenRouterModelRegistry
8
9
  from .config import OpenRouterConfig
10
+ from .schema import format_response
11
+ from pydantic import BaseModel
9
12
  import logging
10
13
  import time
11
14
  import asyncio
@@ -140,7 +143,7 @@ class OpenRouterClient:
140
143
  retry_initial_delay=retry_initial_delay,
141
144
  retry_max_delay=retry_max_delay,
142
145
  )
143
-
146
+
144
147
  self._client = OpenAI(
145
148
  api_key=token,
146
149
  base_url=self.config.base_url,
@@ -165,7 +168,7 @@ class OpenRouterClient:
165
168
  stack_trace = traceback.format_exc()
166
169
  logger.error(f"API error during {operation}: {error_msg}")
167
170
  logger.error(f"Stack trace:\n{stack_trace}")
168
-
171
+
169
172
  if isinstance(error, OpenAIError):
170
173
  raise OpenRouterAPIError(f"API error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
171
174
  elif "Connection" in error_msg:
@@ -213,37 +216,37 @@ class OpenRouterClient:
213
216
  # Log the request details
214
217
  logger.info(f"Sending chat completion request to OpenRouter with model: {model or self.model}")
215
218
  logger.info(f"Message count: {len(messages)}")
216
-
219
+
217
220
  # Calculate total message size for logging
218
221
  total_size = sum(len(str(msg)) for msg in messages)
219
222
  logger.info(f"Total message size: {total_size} bytes")
220
-
223
+
221
224
  request = {
222
225
  "model": model or self.model,
223
226
  "messages": messages,
224
227
  "stream": stream,
225
228
  **kwargs,
226
229
  }
227
-
230
+
228
231
  response = self._client.chat.completions.create(**request)
229
-
232
+
230
233
  if response is None:
231
234
  logger.error("Received None response from OpenRouter API")
232
235
  raise OpenRouterAPIError("Received None response from OpenRouter API")
233
-
234
- logger.info(f"Response type: {type(response)}")
235
- logger.info(f"Response attributes: {dir(response)}")
236
- logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
237
-
236
+
237
+ logger.debug(f"Response type: {type(response)}")
238
+ logger.debug(f"Response attributes: {dir(response)}")
239
+ logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
240
+
238
241
  return response
239
-
242
+
240
243
  except Exception as e:
241
244
  stack_trace = traceback.format_exc()
242
245
  logger.error(f"Error in chat completion: {str(e)}")
243
246
  logger.error(f"Stack trace:\n{stack_trace}")
244
247
  logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
245
248
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
246
-
249
+
247
250
  if hasattr(e, 'response') and e.response is not None:
248
251
  logger.error(f"Response status: {e.response.status_code}")
249
252
  logger.error(f"Response headers: {e.response.headers}")
@@ -265,27 +268,27 @@ class OpenRouterClient:
265
268
  **kwargs: Any,
266
269
  ) -> Any:
267
270
  """Get a chat completion from OpenRouter."""
268
-
271
+
269
272
  request = {
270
273
  "model": model or self.model,
271
274
  "messages": messages,
272
275
  "response_format": response_format,
273
276
  **kwargs,
274
277
  }
275
-
278
+
276
279
  # Log the full request for debugging
277
280
  logger.debug(f"Full request: {request}")
278
-
281
+
279
282
  try:
280
283
  # Log the request details
281
284
  logger.info(f"Sending parse request to OpenRouter with model: {model or self.model}")
282
285
  logger.info(f"Message count: {len(messages)}")
283
286
  logger.info(f"Response format: {response_format}")
284
-
287
+
285
288
  # Calculate total message size for logging
286
289
  total_size = sum(len(str(msg)) for msg in messages)
287
290
  logger.info(f"Total message size: {total_size} bytes")
288
-
291
+
289
292
  try:
290
293
  response = self._client.beta.chat.completions.parse(**request)
291
294
  except RateLimitError as e:
@@ -306,17 +309,17 @@ class OpenRouterClient:
306
309
  except OpenAIError as e:
307
310
  logger.error(f"OpenAI error: {str(e)}")
308
311
  raise OpenRouterAPIError(f"OpenAI error: {str(e)}")
309
-
312
+
310
313
  # Log raw response for debugging
311
314
  logger.debug(f"Raw response: {response}")
312
315
  if hasattr(response, '__dict__'):
313
316
  logger.debug(f"Response attributes: {dir(response)}")
314
317
  logger.debug(f"Response dict: {response.__dict__}")
315
-
318
+
316
319
  if response is None:
317
320
  logger.error("Received None response from OpenRouter API")
318
321
  raise OpenRouterAPIError("Received None response from OpenRouter API")
319
-
322
+
320
323
  # Try to get the raw response content if available
321
324
  if hasattr(response, '_response'):
322
325
  try:
@@ -324,25 +327,25 @@ class OpenRouterClient:
324
327
  logger.debug(f"Raw response content: {raw_content[:1000]}...")
325
328
  except Exception as e:
326
329
  logger.debug(f"Could not get raw response content: {e}")
327
-
330
+
328
331
  # Validate response structure
329
332
  if not hasattr(response, 'choices'):
330
333
  logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
331
334
  raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
332
-
335
+
333
336
  if not response.choices:
334
337
  logger.error("Response has empty choices list")
335
338
  raise OpenRouterAPIError("Invalid response format: empty choices list")
336
-
339
+
337
340
  if not hasattr(response.choices[0], 'message'):
338
341
  logger.error(f"First choice missing 'message' attribute. Available attributes: {dir(response.choices[0])}")
339
342
  raise OpenRouterAPIError("Invalid response format: missing 'message' attribute in first choice")
340
-
343
+
341
344
  # Check if the message has a parsed attribute or content
342
345
  if not hasattr(response.choices[0].message, 'parsed') and not hasattr(response.choices[0].message, 'content'):
343
346
  logger.error(f"Message missing both 'parsed' and 'content' attributes. Available attributes: {dir(response.choices[0].message)}")
344
347
  raise OpenRouterAPIError("Invalid response format: message must have either 'parsed' or 'content' attribute")
345
-
348
+
346
349
  # If there's no parsed attribute but there is content, try to parse it
347
350
  if not hasattr(response.choices[0].message, 'parsed') and hasattr(response.choices[0].message, 'content'):
348
351
  try:
@@ -357,18 +360,17 @@ class OpenRouterClient:
357
360
  logger.error(f"Failed to parse message content: {str(e)}")
358
361
  logger.error(f"Stack trace:\n{stack_trace}")
359
362
  raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}\nStack trace:\n{stack_trace}")
360
-
363
+
361
364
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
362
-
365
+
363
366
  return response
364
-
367
+
365
368
  except Exception as e:
366
369
  stack_trace = traceback.format_exc()
367
370
  logger.error(f"Raising error: {e}")
368
- logger.error(f"Full request from ERROR:\n{json.dumps(request, indent=2, cls=CustomJSONEncoder)}")
369
371
  logger.error(f"Error in parse completion: {str(e)}")
370
372
  logger.error(f"Stack trace:\n{stack_trace}")
371
-
373
+
372
374
  if hasattr(e, 'response') and e.response is not None:
373
375
  logger.error(f"Response status: {e.response.status_code}")
374
376
  logger.error(f"Response headers: {e.response.headers}")
@@ -380,6 +382,79 @@ class OpenRouterClient:
380
382
  logger.error("Could not read response content")
381
383
  self._handle_api_error("parse completion", e)
382
384
 
385
+ @with_retry()
386
+ def create_parsed(
387
+ self,
388
+ messages: list[dict[str, Any]],
389
+ response_format: Type[BaseModel],
390
+ *,
391
+ model: str | None = None,
392
+ stream: bool = False,
393
+ **kwargs: Any,
394
+ ) -> Any:
395
+ """Get a chat completion from OpenRouter with structured output.
396
+
397
+ Args:
398
+ messages: The messages to send to the model
399
+ response_format: A Pydantic model defining the expected response format
400
+ model: Optional model override
401
+ stream: Whether to stream the response
402
+ **kwargs: Additional arguments to pass to the API
403
+
404
+ Returns:
405
+ The parsed response from the model
406
+ """
407
+ try:
408
+ # Convert Pydantic model to OpenAI response format
409
+ response_format_param = type_to_response_format_param(response_format)
410
+
411
+ # Log the request details
412
+ logger.info(f"Sending parsed chat completion request to OpenRouter with model: {model or self.model}")
413
+ logger.info(f"Message count: {len(messages)}")
414
+ logger.info(f"Response format: {json.dumps(response_format_param, indent=2)}")
415
+
416
+ # Calculate total message size for logging
417
+ total_size = sum(len(str(msg)) for msg in messages)
418
+ logger.info(f"Total message size: {total_size} bytes")
419
+
420
+ request = {
421
+ "model": model or self.model,
422
+ "messages": messages,
423
+ "stream": stream,
424
+ "response_format": response_format_param,
425
+ **kwargs,
426
+ }
427
+
428
+ response = self._client.chat.completions.create(**request)
429
+
430
+ if response is None:
431
+ logger.error("Received None response from OpenRouter API")
432
+ raise OpenRouterAPIError("Received None response from OpenRouter API")
433
+
434
+ logger.debug(f"Response type: {type(response)}")
435
+ logger.debug(f"Response attributes: {dir(response)}")
436
+ logger.debug(f"Received response from OpenRouter: {len(response.choices)} choices")
437
+
438
+ return response
439
+
440
+ except Exception as e:
441
+ stack_trace = traceback.format_exc()
442
+ logger.error(f"Error in parsed chat completion: {str(e)}")
443
+ logger.error(f"Stack trace:\n{stack_trace}")
444
+ logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
445
+ logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
446
+
447
+ if hasattr(e, 'response') and e.response is not None:
448
+ logger.error(f"Response status: {e.response.status_code}")
449
+ logger.error(f"Response headers: {e.response.headers}")
450
+ try:
451
+ content = e.response.text
452
+ logger.error(f"Response content length: {len(content)} bytes")
453
+ logger.error(f"Response content preview: {content[:1000]}...")
454
+ except:
455
+ logger.error("Could not read response content")
456
+ self._handle_api_error("parsed chat completion", e)
457
+
383
458
  @classmethod
384
459
  def register_model(cls, name: str, value: str) -> None:
385
460
  """Register a new model.
@@ -400,4 +475,4 @@ class OpenRouterClient:
400
475
  Returns:
401
476
  A dictionary mapping model names to their identifiers.
402
477
  """
403
- return OpenRouterModelRegistry.list_models()
478
+ return OpenRouterModelRegistry.list_models()
@@ -0,0 +1,21 @@
1
+ """
2
+ Response formatting utilities for OpenRouter.
3
+ """
4
+
5
+ from typing import Any
6
+ import json
7
+
8
+ def format_response(response: Any) -> dict[str, Any]:
9
+ """Format the response into a JSON-serializable dictionary.
10
+
11
+ Args:
12
+ response: The response from OpenAI
13
+
14
+ Returns:
15
+ A JSON-serializable dictionary containing the response data
16
+ """
17
+ if hasattr(response, 'model_dump'):
18
+ return response.model_dump()
19
+ elif hasattr(response, '__dict__'):
20
+ return response.__dict__
21
+ return str(response)
mbxai/tools/client.py CHANGED
@@ -232,10 +232,9 @@ class ToolClient:
232
232
 
233
233
  # Get all function calls from the output
234
234
  tool_calls = response.choices[0].message.tool_calls
235
- logger.info(f"Tool calls: {tool_calls}")
236
235
  # Process function calls if any
237
236
  if tool_calls:
238
- logger.info(f"Processing {len(tool_calls)} function calls")
237
+ logger.info(f"Tool calls: {len(tool_calls)} - Tools: {[tc.function.name for tc in tool_calls]}")
239
238
 
240
239
  # Process each function call
241
240
  for tool_call in tool_calls:
@@ -282,7 +281,6 @@ class ToolClient:
282
281
  logger.info("Final response")
283
282
  return response
284
283
 
285
-
286
284
  def parse(
287
285
  self,
288
286
  messages: list[dict[str, Any]],
@@ -300,7 +298,7 @@ class ToolClient:
300
298
 
301
299
  while True:
302
300
  # Get the model's response
303
- response = self._client.parse(
301
+ response = self._client.create_parsed(
304
302
  messages=messages,
305
303
  response_format=response_format,
306
304
  model=model,
@@ -317,10 +315,9 @@ class ToolClient:
317
315
 
318
316
  # Get all function calls from the output
319
317
  tool_calls = response.choices[0].message.tool_calls
320
- logger.info(f"Tool calls: {tool_calls}")
321
318
  # Process function calls if any
322
319
  if tool_calls:
323
- logger.info(f"Processing {len(tool_calls)} function calls")
320
+ logger.info(f"Tool calls: {len(tool_calls)} - Tools: {[tc.function.name for tc in tool_calls]}")
324
321
 
325
322
  # Process each function call
326
323
  for tool_call in tool_calls:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.0.13
3
+ Version: 1.2.0
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -1,24 +1,28 @@
1
- mbxai/__init__.py,sha256=xn7_fLvixGExmDMnngCrvzwV3R4XDAWFj2SpGThziKw,48
1
+ mbxai/__init__.py,sha256=JfbYRtbxrVf4tf03gAoAUdJuD9DAHhCaIHSgPc4Gnss,47
2
2
  mbxai/core.py,sha256=WMvmU9TTa7M_m-qWsUew4xH8Ul6xseCZ2iBCXJTW-Bs,196
3
3
  mbxai/examples/openrouter_example.py,sha256=-grXHKMmFLoh-yUIEMc31n8Gg1S7uSazBWCIOWxgbyQ,1317
4
4
  mbxai/examples/parse_example.py,sha256=eCKMJoOl6qwo8sDP6Trc6ncgjPlgTqi5tPE2kB5_P0k,3821
5
5
  mbxai/examples/parse_tool_example.py,sha256=duHN8scI9ZK6XZ5hdiz1Adzyc-_7tH9Ls9qP4S0bf5s,5477
6
+ mbxai/examples/request.json,sha256=fjVMses305wVUXgcmjESCvPgP81Js8Kk6zHjZ8EDyEg,5434
7
+ mbxai/examples/response.json,sha256=4SGJJyQjWWeN__Mrxm6ZtHIo1NUtLEheldd5KaA2mHw,856
8
+ mbxai/examples/send_request.py,sha256=O5gCHUHy7RvkEFo9IQATgnSOfOdu8OqKHfjAlLDwWPg,6023
6
9
  mbxai/examples/tool_client_example.py,sha256=9DNaejXLA85dPbExMiv5y76qlFhzOJF9E5EnMOsy_Dc,3993
7
10
  mbxai/examples/mcp/mcp_client_example.py,sha256=R4H-OU5FvGL41cCkTdLa3bocsmVJYQYOcOHRf61nbZc,2822
8
11
  mbxai/examples/mcp/mcp_server_example.py,sha256=nFfg22Jnc6HMW_ezLO3So1xwDdx2_rItj5CR-y_Nevs,3966
9
12
  mbxai/mcp/__init__.py,sha256=_ek9iYdYqW5saKetj4qDci11jxesQDiHPJRpHMKkxgU,175
10
13
  mbxai/mcp/client.py,sha256=eXIN2ebprNF5UgM1jb-4JkXmc-5toUhtlBNFKVU7FgY,5204
11
14
  mbxai/mcp/example.py,sha256=oaol7AvvZnX86JWNz64KvPjab5gg1VjVN3G8eFSzuaE,2350
12
- mbxai/mcp/server.py,sha256=AKLHz9aIuqN_oMtaY14-P_1n_TKlTYLUhPQ4Cu3cWWk,3455
15
+ mbxai/mcp/server.py,sha256=gB10CpbzibEt2XethhIyVLcfgu9GZXrSqKRcsjHfOug,3454
13
16
  mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
14
- mbxai/openrouter/client.py,sha256=7EL-1f3GCHi6QSidU3fAXrEncrk-6qWVyjB5IvJio3o,16947
17
+ mbxai/openrouter/client.py,sha256=P_TBb980EgEORGUz9cMqNAAs3R18EHFYgcE92TNI2bo,19881
15
18
  mbxai/openrouter/config.py,sha256=Ia93s-auim9Sq71eunVDbn9ET5xX2zusXpV4JBdHAzs,3251
16
19
  mbxai/openrouter/models.py,sha256=b3IjjtZAjeGOf2rLsdnCD1HacjTnS8jmv_ZXorc-KJQ,2604
20
+ mbxai/openrouter/schema.py,sha256=H_77ZrA9zmbX155bWpCJj1jehUyJPS0QybEW1IVAoe0,540
17
21
  mbxai/tools/__init__.py,sha256=ogxrHvgJ7OR62Lmd5x9Eh5d2C0jqWyQis7Zy3yKpZ78,218
18
- mbxai/tools/client.py,sha256=h_1fxVDBq57f_OXNsj-TBp6-r367sv6Z5nk1qLFcLO8,14951
22
+ mbxai/tools/client.py,sha256=SxXSxvvglbzSaruqm_mebdky8RY2E3hW1VldQbfa0Cw,14914
19
23
  mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
20
24
  mbxai/tools/types.py,sha256=pAoVuL7nKhvL3Iek0JheGfll4clsABFLl1CNjmiG3No,5866
21
- mbxai-1.0.13.dist-info/METADATA,sha256=-EEFhf9BHg5JwFiG--JTFSV0W65n0NXCWez7braC_ZQ,4148
22
- mbxai-1.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mbxai-1.0.13.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
24
- mbxai-1.0.13.dist-info/RECORD,,
25
+ mbxai-1.2.0.dist-info/METADATA,sha256=DFvNZD8bWCBKWSBm3H5wnT1bGnrJagJj9qfJfwgodtY,4147
26
+ mbxai-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ mbxai-1.2.0.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
28
+ mbxai-1.2.0.dist-info/RECORD,,
File without changes