mbxai 1.0.12__py3-none-any.whl → 1.1.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.12"
5
+ __version__ = "1.1.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.12",
34
+ version="1.1.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
@@ -90,6 +93,16 @@ def with_retry(max_retries: int = 3, initial_delay: float = 1.0, max_delay: floa
90
93
  return decorator
91
94
 
92
95
 
96
+ class CustomJSONEncoder(json.JSONEncoder):
97
+ """Custom JSON encoder to handle special types."""
98
+ def default(self, obj):
99
+ if hasattr(obj, '__name__'):
100
+ return obj.__name__
101
+ if hasattr(obj, '__dict__'):
102
+ return obj.__dict__
103
+ return str(obj)
104
+
105
+
93
106
  class OpenRouterClient:
94
107
  """Client for interacting with the OpenRouter API."""
95
108
 
@@ -130,7 +143,7 @@ class OpenRouterClient:
130
143
  retry_initial_delay=retry_initial_delay,
131
144
  retry_max_delay=retry_max_delay,
132
145
  )
133
-
146
+
134
147
  self._client = OpenAI(
135
148
  api_key=token,
136
149
  base_url=self.config.base_url,
@@ -155,7 +168,7 @@ class OpenRouterClient:
155
168
  stack_trace = traceback.format_exc()
156
169
  logger.error(f"API error during {operation}: {error_msg}")
157
170
  logger.error(f"Stack trace:\n{stack_trace}")
158
-
171
+
159
172
  if isinstance(error, OpenAIError):
160
173
  raise OpenRouterAPIError(f"API error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
161
174
  elif "Connection" in error_msg:
@@ -203,37 +216,37 @@ class OpenRouterClient:
203
216
  # Log the request details
204
217
  logger.info(f"Sending chat completion request to OpenRouter with model: {model or self.model}")
205
218
  logger.info(f"Message count: {len(messages)}")
206
-
219
+
207
220
  # Calculate total message size for logging
208
221
  total_size = sum(len(str(msg)) for msg in messages)
209
222
  logger.info(f"Total message size: {total_size} bytes")
210
-
223
+
211
224
  request = {
212
225
  "model": model or self.model,
213
226
  "messages": messages,
214
227
  "stream": stream,
215
228
  **kwargs,
216
229
  }
217
-
230
+
218
231
  response = self._client.chat.completions.create(**request)
219
-
232
+
220
233
  if response is None:
221
234
  logger.error("Received None response from OpenRouter API")
222
235
  raise OpenRouterAPIError("Received None response from OpenRouter API")
223
-
236
+
224
237
  logger.info(f"Response type: {type(response)}")
225
238
  logger.info(f"Response attributes: {dir(response)}")
226
239
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
227
-
240
+
228
241
  return response
229
-
242
+
230
243
  except Exception as e:
231
244
  stack_trace = traceback.format_exc()
232
245
  logger.error(f"Error in chat completion: {str(e)}")
233
246
  logger.error(f"Stack trace:\n{stack_trace}")
234
247
  logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
235
248
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
236
-
249
+
237
250
  if hasattr(e, 'response') and e.response is not None:
238
251
  logger.error(f"Response status: {e.response.status_code}")
239
252
  logger.error(f"Response headers: {e.response.headers}")
@@ -255,27 +268,27 @@ class OpenRouterClient:
255
268
  **kwargs: Any,
256
269
  ) -> Any:
257
270
  """Get a chat completion from OpenRouter."""
258
-
271
+
259
272
  request = {
260
273
  "model": model or self.model,
261
274
  "messages": messages,
262
275
  "response_format": response_format,
263
276
  **kwargs,
264
277
  }
265
-
278
+
266
279
  # Log the full request for debugging
267
280
  logger.debug(f"Full request: {request}")
268
-
281
+
269
282
  try:
270
283
  # Log the request details
271
284
  logger.info(f"Sending parse request to OpenRouter with model: {model or self.model}")
272
285
  logger.info(f"Message count: {len(messages)}")
273
286
  logger.info(f"Response format: {response_format}")
274
-
287
+
275
288
  # Calculate total message size for logging
276
289
  total_size = sum(len(str(msg)) for msg in messages)
277
290
  logger.info(f"Total message size: {total_size} bytes")
278
-
291
+
279
292
  try:
280
293
  response = self._client.beta.chat.completions.parse(**request)
281
294
  except RateLimitError as e:
@@ -296,17 +309,17 @@ class OpenRouterClient:
296
309
  except OpenAIError as e:
297
310
  logger.error(f"OpenAI error: {str(e)}")
298
311
  raise OpenRouterAPIError(f"OpenAI error: {str(e)}")
299
-
312
+
300
313
  # Log raw response for debugging
301
314
  logger.debug(f"Raw response: {response}")
302
315
  if hasattr(response, '__dict__'):
303
316
  logger.debug(f"Response attributes: {dir(response)}")
304
317
  logger.debug(f"Response dict: {response.__dict__}")
305
-
318
+
306
319
  if response is None:
307
320
  logger.error("Received None response from OpenRouter API")
308
321
  raise OpenRouterAPIError("Received None response from OpenRouter API")
309
-
322
+
310
323
  # Try to get the raw response content if available
311
324
  if hasattr(response, '_response'):
312
325
  try:
@@ -314,25 +327,25 @@ class OpenRouterClient:
314
327
  logger.debug(f"Raw response content: {raw_content[:1000]}...")
315
328
  except Exception as e:
316
329
  logger.debug(f"Could not get raw response content: {e}")
317
-
330
+
318
331
  # Validate response structure
319
332
  if not hasattr(response, 'choices'):
320
333
  logger.error(f"Response missing 'choices' attribute. Available attributes: {dir(response)}")
321
334
  raise OpenRouterAPIError("Invalid response format: missing 'choices' attribute")
322
-
335
+
323
336
  if not response.choices:
324
337
  logger.error("Response has empty choices list")
325
338
  raise OpenRouterAPIError("Invalid response format: empty choices list")
326
-
339
+
327
340
  if not hasattr(response.choices[0], 'message'):
328
341
  logger.error(f"First choice missing 'message' attribute. Available attributes: {dir(response.choices[0])}")
329
342
  raise OpenRouterAPIError("Invalid response format: missing 'message' attribute in first choice")
330
-
343
+
331
344
  # Check if the message has a parsed attribute or content
332
345
  if not hasattr(response.choices[0].message, 'parsed') and not hasattr(response.choices[0].message, 'content'):
333
346
  logger.error(f"Message missing both 'parsed' and 'content' attributes. Available attributes: {dir(response.choices[0].message)}")
334
347
  raise OpenRouterAPIError("Invalid response format: message must have either 'parsed' or 'content' attribute")
335
-
348
+
336
349
  # If there's no parsed attribute but there is content, try to parse it
337
350
  if not hasattr(response.choices[0].message, 'parsed') and hasattr(response.choices[0].message, 'content'):
338
351
  try:
@@ -347,18 +360,17 @@ class OpenRouterClient:
347
360
  logger.error(f"Failed to parse message content: {str(e)}")
348
361
  logger.error(f"Stack trace:\n{stack_trace}")
349
362
  raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}\nStack trace:\n{stack_trace}")
350
-
363
+
351
364
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
352
-
365
+
353
366
  return response
354
-
367
+
355
368
  except Exception as e:
356
369
  stack_trace = traceback.format_exc()
357
370
  logger.error(f"Raising error: {e}")
358
- logger.error(f"Full request from ERROR:\n{json.dumps(request, indent=2)}")
359
371
  logger.error(f"Error in parse completion: {str(e)}")
360
372
  logger.error(f"Stack trace:\n{stack_trace}")
361
-
373
+
362
374
  if hasattr(e, 'response') and e.response is not None:
363
375
  logger.error(f"Response status: {e.response.status_code}")
364
376
  logger.error(f"Response headers: {e.response.headers}")
@@ -370,6 +382,79 @@ class OpenRouterClient:
370
382
  logger.error("Could not read response content")
371
383
  self._handle_api_error("parse completion", e)
372
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.info(f"Response type: {type(response)}")
435
+ logger.info(f"Response attributes: {dir(response)}")
436
+ logger.info(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
+
373
458
  @classmethod
374
459
  def register_model(cls, name: str, value: str) -> None:
375
460
  """Register a new model.
@@ -390,4 +475,4 @@ class OpenRouterClient:
390
475
  Returns:
391
476
  A dictionary mapping model names to their identifiers.
392
477
  """
393
- 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
@@ -282,7 +282,6 @@ class ToolClient:
282
282
  logger.info("Final response")
283
283
  return response
284
284
 
285
-
286
285
  def parse(
287
286
  self,
288
287
  messages: list[dict[str, Any]],
@@ -300,7 +299,7 @@ class ToolClient:
300
299
 
301
300
  while True:
302
301
  # Get the model's response
303
- response = self._client.parse(
302
+ response = self._client.create_parsed(
304
303
  messages=messages,
305
304
  response_format=response_format,
306
305
  model=model,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.0.12
3
+ Version: 1.1.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=Tt4H1XboHHlIeLHSX5mcSCD1CmE4PLQ8L2jidznrytg,48
1
+ mbxai/__init__.py,sha256=_2qGr1Yic6rhVzF_j8_SfajJN6gzgVi6j6QA2iK59wQ,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=-umh9KNn0dep-C4Rk4qicrQxawBn3H5Op0Vf7m04ZK4,3455
15
+ mbxai/mcp/server.py,sha256=V8yGrpOzLJgNzK9YhwhLN_ETAyP0SUpZvxENlcxlHiU,3454
13
16
  mbxai/openrouter/__init__.py,sha256=Ito9Qp_B6q-RLGAQcYyTJVWwR2YAZvNqE-HIYXxhtD8,298
14
- mbxai/openrouter/client.py,sha256=5M0a7lL2XKdBdbNN7gIpA2Uch28AQ4mKr1DkGBi1aBs,16634
17
+ mbxai/openrouter/client.py,sha256=zVibH-BoHsv92RAuwqmTeLUIlAPjEqu3mMNzsRzfCFU,19875
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=TS2ZYYbJPDmGQ1-1ikuDY0_LiwLPhOQ1cy4pIS2UeEA,14946
19
23
  mbxai/tools/example.py,sha256=1HgKK39zzUuwFbnp3f0ThyWVfA_8P28PZcTwaUw5K78,2232
20
24
  mbxai/tools/types.py,sha256=pAoVuL7nKhvL3Iek0JheGfll4clsABFLl1CNjmiG3No,5866
21
- mbxai-1.0.12.dist-info/METADATA,sha256=1WO8gDKfVAQsdYxVlJtE_wBBzKjkroUtE_E4Yx4byMg,4148
22
- mbxai-1.0.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mbxai-1.0.12.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
24
- mbxai-1.0.12.dist-info/RECORD,,
25
+ mbxai-1.1.0.dist-info/METADATA,sha256=cKWnzgZ223Q-xqZbw3Ra_outHqDwOWmr6B5K9C0ntK8,4147
26
+ mbxai-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ mbxai-1.1.0.dist-info/licenses/LICENSE,sha256=hEyhc4FxwYo3NQ40yNgZ7STqwVk-1_XcTXOnAPbGJAw,1069
28
+ mbxai-1.1.0.dist-info/RECORD,,
File without changes