mbxai 1.0.13__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 +1 -1
- mbxai/examples/request.json +127 -0
- mbxai/examples/response.json +30 -0
- mbxai/examples/send_request.py +181 -0
- mbxai/mcp/server.py +1 -1
- mbxai/openrouter/client.py +105 -30
- mbxai/openrouter/schema.py +21 -0
- mbxai/tools/client.py +1 -2
- {mbxai-1.0.13.dist-info → mbxai-1.1.0.dist-info}/METADATA +1 -1
- {mbxai-1.0.13.dist-info → mbxai-1.1.0.dist-info}/RECORD +12 -8
- {mbxai-1.0.13.dist-info → mbxai-1.1.0.dist-info}/WHEEL +0 -0
- {mbxai-1.0.13.dist-info → mbxai-1.1.0.dist-info}/licenses/LICENSE +0 -0
mbxai/__init__.py
CHANGED
@@ -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
mbxai/openrouter/client.py
CHANGED
@@ -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
|
-
|
236
|
+
|
234
237
|
logger.info(f"Response type: {type(response)}")
|
235
238
|
logger.info(f"Response attributes: {dir(response)}")
|
236
239
|
logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
|
237
|
-
|
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.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
|
+
|
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
@@ -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.
|
302
|
+
response = self._client.create_parsed(
|
304
303
|
messages=messages,
|
305
304
|
response_format=response_format,
|
306
305
|
model=model,
|
@@ -1,24 +1,28 @@
|
|
1
|
-
mbxai/__init__.py,sha256=
|
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=
|
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=
|
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=
|
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.
|
22
|
-
mbxai-1.0.
|
23
|
-
mbxai-1.0.
|
24
|
-
mbxai-1.0.
|
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
|
File without changes
|