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 +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 +115 -30
- mbxai/openrouter/schema.py +21 -0
- mbxai/tools/client.py +1 -2
- {mbxai-1.0.12.dist-info → mbxai-1.1.0.dist-info}/METADATA +1 -1
- {mbxai-1.0.12.dist-info → mbxai-1.1.0.dist-info}/RECORD +12 -8
- {mbxai-1.0.12.dist-info → mbxai-1.1.0.dist-info}/WHEEL +0 -0
- {mbxai-1.0.12.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
|
@@ -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.
|
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
|