mbxai 1.0.4__tar.gz → 1.0.6__tar.gz

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.
Files changed (27) hide show
  1. {mbxai-1.0.4 → mbxai-1.0.6}/PKG-INFO +1 -1
  2. {mbxai-1.0.4 → mbxai-1.0.6}/pyproject.toml +2 -2
  3. {mbxai-1.0.4 → mbxai-1.0.6}/setup.py +1 -1
  4. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/__init__.py +1 -1
  5. mbxai-1.0.6/src/mbxai/examples/mcp/mcp_server_example.py +118 -0
  6. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/mcp/client.py +2 -2
  7. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/mcp/server.py +8 -8
  8. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/openrouter/client.py +16 -6
  9. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/tools/types.py +12 -12
  10. mbxai-1.0.4/src/mbxai/examples/mcp/mcp_server_example.py +0 -94
  11. {mbxai-1.0.4 → mbxai-1.0.6}/.gitignore +0 -0
  12. {mbxai-1.0.4 → mbxai-1.0.6}/LICENSE +0 -0
  13. {mbxai-1.0.4 → mbxai-1.0.6}/README.md +0 -0
  14. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/core.py +0 -0
  15. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/examples/mcp/mcp_client_example.py +0 -0
  16. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/examples/openrouter_example.py +0 -0
  17. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/examples/parse_example.py +0 -0
  18. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/examples/parse_tool_example.py +0 -0
  19. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/examples/tool_client_example.py +0 -0
  20. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/mcp/__init__.py +0 -0
  21. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/mcp/example.py +0 -0
  22. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/openrouter/__init__.py +0 -0
  23. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/openrouter/config.py +0 -0
  24. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/openrouter/models.py +0 -0
  25. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/tools/__init__.py +0 -0
  26. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/tools/client.py +0 -0
  27. {mbxai-1.0.4 → mbxai-1.0.6}/src/mbxai/tools/example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mbxai
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: MBX AI SDK
5
5
  Project-URL: Homepage, https://www.mibexx.de
6
6
  Project-URL: Documentation, https://www.mibexx.de
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mbxai"
7
- version = "1.0.4"
7
+ version = "1.0.6"
8
8
  authors = [
9
9
  { name = "MBX AI" }
10
10
  ]
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "starlette>=0.46.2",
31
31
  "uvicorn>=0.34.2",
32
32
  "pydantic-settings>=2.9.1",
33
- "typing-inspection<=0.4.0"
33
+ "typing-inspection<=0.4.0",
34
34
  ]
35
35
 
36
36
  [project.urls]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="mbxai",
5
- version="1.0.4",
5
+ version="1.0.6",
6
6
  author="MBX AI",
7
7
  description="MBX AI SDK",
8
8
  long_description=open("README.md").read(),
@@ -2,4 +2,4 @@
2
2
  MBX AI package.
3
3
  """
4
4
 
5
- __version__ = "1.0.4"
5
+ __version__ = "1.0.6"
@@ -0,0 +1,118 @@
1
+ """Example MCP server implementation."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+ from pydantic import BaseModel
6
+ from mcp.server.fastmcp import FastMCP
7
+ from mbxai.mcp import MCPServer
8
+ from fastapi import Body
9
+ import uvicorn
10
+
11
+ # Create a FastMCP instance for this module
12
+ mcp = FastMCP("html-structure-analyser")
13
+
14
+ class ScraperInput(BaseModel):
15
+ url: str
16
+
17
+ @mcp.tool()
18
+ async def scrape_html(input: ScraperInput) -> str:
19
+ """Scrape HTML content from a URL.
20
+
21
+ This function fetches the HTML content from a given URL using httpx.
22
+ It handles redirects and raises appropriate exceptions for HTTP errors.
23
+
24
+ Args:
25
+ input: ScraperInput model containing the URL to scrape
26
+
27
+ Returns:
28
+ str: The HTML content of the page
29
+
30
+ Raises:
31
+ httpx.HTTPError: If there's an HTTP error while fetching the page
32
+ Exception: For any other unexpected errors
33
+ """
34
+ # This is a mock implementation that returns sample HTML
35
+ sample_html = f"""
36
+ <!DOCTYPE html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="UTF-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
41
+ <title>Sample Page</title>
42
+ </head>
43
+ <body>
44
+ <header>
45
+ <h1>Welcome to Sample Page</h1>
46
+ <nav>
47
+ <ul>
48
+ <li><a href="#home">Home</a></li>
49
+ <li><a href="#about">About</a></li>
50
+ <li><a href="#contact">Contact</a></li>
51
+ </ul>
52
+ </nav>
53
+ </header>
54
+ <main>
55
+ <section id="content">
56
+ <h2>Main Content</h2>
57
+ <p>This is a sample HTML page that was scraped from {input.url}</p>
58
+ <article>
59
+ <h3>Article Title</h3>
60
+ <p>This is a sample article with some content.</p>
61
+ </article>
62
+ </section>
63
+ </main>
64
+ <footer>
65
+ <p>&copy; 2024 Sample Website</p>
66
+ </footer>
67
+ </body>
68
+ </html>
69
+ """
70
+ return sample_html
71
+
72
+ class CustomMCPServer(MCPServer):
73
+ """Custom MCP server with overridden endpoints."""
74
+
75
+ def _register_endpoints(self) -> None:
76
+ """Register FastAPI endpoints."""
77
+ @self.app.get("/tools")
78
+ async def get_tools():
79
+ """Return the list of available tools."""
80
+ tools = []
81
+ for tool in self._tools.values():
82
+ tool_dict = tool.model_dump(exclude={'function'})
83
+ tool_dict['internal_url'] = f"http://localhost:8000/tools/{tool.name}/invoke"
84
+ tool_dict['service'] = "html-structure-analyser"
85
+ tools.append(tool_dict)
86
+ return {"tools": tools}
87
+
88
+ @self.app.post("/tools/{tool_name}/invoke")
89
+ async def invoke_tool(tool_name: str, arguments: dict[str, Any] = Body(...)):
90
+ """Invoke a specific MCP tool."""
91
+ try:
92
+ result = await self.mcp_server.call_tool(tool_name, arguments=arguments)
93
+ if isinstance(result, list) and len(result) == 1:
94
+ first_item = result[0]
95
+ if hasattr(first_item, "type") and first_item.type == "text":
96
+ return first_item.text
97
+ elif isinstance(result, dict) and result.get("type") == "text":
98
+ return result["text"]
99
+ return result
100
+ except Exception as e:
101
+ return {"error": f"Error invoking tool {tool_name}: {str(e)}"}
102
+
103
+ async def start_server():
104
+ # Create and start the MCP server
105
+ server = CustomMCPServer("html-structure-analyser")
106
+
107
+ # Register the tool with the MCP server
108
+ await server.add_tool(scrape_html)
109
+
110
+ # Create uvicorn config
111
+ config = uvicorn.Config(server.app, host="0.0.0.0", port=8000)
112
+ server = uvicorn.Server(config)
113
+
114
+ # Start the server
115
+ await server.serve()
116
+
117
+ if __name__ == "__main__":
118
+ asyncio.run(start_server())
@@ -16,7 +16,7 @@ T = TypeVar("T", bound=BaseModel)
16
16
 
17
17
  class MCPTool(Tool):
18
18
  """A tool from the MCP server."""
19
- input_schema: dict[str, Any]
19
+ inputSchema: dict[str, Any]
20
20
  internal_url: str
21
21
  strict: bool = True
22
22
  function: Callable[..., Any] | None = None # Make function optional during initialization
@@ -24,7 +24,7 @@ class MCPTool(Tool):
24
24
  def to_openai_function(self) -> dict[str, Any]:
25
25
  """Convert the tool to an OpenAI function definition."""
26
26
  # Convert schema to strict format, keeping input wrapper
27
- strict_schema = convert_to_strict_schema(self.input_schema, strict=self.strict, keep_input_wrapper=True)
27
+ strict_schema = convert_to_strict_schema(self.inputSchema, strict=self.strict, keep_input_wrapper=True)
28
28
 
29
29
  function_def = {
30
30
  "type": "function",
@@ -14,7 +14,7 @@ class Tool(BaseModel):
14
14
  model_config = ConfigDict(strict=True)
15
15
  name: str = Field(description="The name of the tool")
16
16
  description: str = Field(description="The description of what the tool does")
17
- input_schema: dict[str, Any] = Field(description="The input schema for the tool")
17
+ inputSchema: dict[str, Any] = Field(description="The input schema for the tool")
18
18
  strict: bool = Field(default=True, description="Whether the tool response is strictly validated")
19
19
  function: Callable[..., Any] = Field(description="The tool function", exclude=True)
20
20
 
@@ -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.4",
34
+ version="1.0.6",
35
35
  )
36
36
 
37
37
  # Initialize MCP server
@@ -75,17 +75,17 @@ class MCPServer:
75
75
  tool_metadata = tools[-1]
76
76
 
77
77
  # Convert FastMCP schema to our schema format
78
- input_schema = tool_metadata.inputSchema
79
- if isinstance(input_schema, dict):
80
- if "$ref" in input_schema:
81
- ref = input_schema["$ref"].split("/")[-1]
82
- input_schema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
78
+ inputSchema = tool_metadata.inputSchema
79
+ if isinstance(inputSchema, dict):
80
+ if "$ref" in inputSchema:
81
+ ref = inputSchema["$ref"].split("/")[-1]
82
+ inputSchema = tool_metadata.inputSchema.get("$defs", {}).get(ref, {})
83
83
 
84
84
  # Create and store Tool instance
85
85
  self._tools[tool_metadata.name] = Tool(
86
86
  name=tool_metadata.name,
87
87
  description=tool_metadata.description,
88
- input_schema=input_schema,
88
+ inputSchema=inputSchema,
89
89
  strict=True,
90
90
  function=tool
91
91
  )
@@ -9,6 +9,7 @@ from .config import OpenRouterConfig
9
9
  import logging
10
10
  import time
11
11
  import asyncio
12
+ import traceback
12
13
  from functools import wraps
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -150,18 +151,20 @@ class OpenRouterClient:
150
151
  OpenRouterError: For other errors
151
152
  """
152
153
  error_msg = str(error)
154
+ stack_trace = traceback.format_exc()
153
155
  logger.error(f"API error during {operation}: {error_msg}")
156
+ logger.error(f"Stack trace:\n{stack_trace}")
154
157
 
155
158
  if isinstance(error, OpenAIError):
156
- raise OpenRouterAPIError(f"API error during {operation}: {error_msg}")
159
+ raise OpenRouterAPIError(f"API error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
157
160
  elif "Connection" in error_msg:
158
- raise OpenRouterConnectionError(f"Connection error during {operation}: {error_msg}")
161
+ raise OpenRouterConnectionError(f"Connection error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
159
162
  elif "Expecting value" in error_msg and "line" in error_msg:
160
163
  # This is a JSON parsing error, likely due to a truncated or malformed response
161
164
  logger.error("JSON parsing error detected. This might be due to a large response or network issues.")
162
- raise OpenRouterAPIError(f"Response parsing error during {operation}. The response might be too large or malformed.")
165
+ raise OpenRouterAPIError(f"Response parsing error during {operation}. The response might be too large or malformed.\nStack trace:\n{stack_trace}")
163
166
  else:
164
- raise OpenRouterError(f"Error during {operation}: {error_msg}")
167
+ raise OpenRouterError(f"Error during {operation}: {error_msg}\nStack trace:\n{stack_trace}")
165
168
 
166
169
  @property
167
170
  def model(self) -> str:
@@ -224,7 +227,9 @@ class OpenRouterClient:
224
227
  return response
225
228
 
226
229
  except Exception as e:
230
+ stack_trace = traceback.format_exc()
227
231
  logger.error(f"Error in chat completion: {str(e)}")
232
+ logger.error(f"Stack trace:\n{stack_trace}")
228
233
  logger.error(f"Request details: model={model or self.model}, stream={stream}, kwargs={kwargs}")
229
234
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
230
235
 
@@ -268,7 +273,7 @@ class OpenRouterClient:
268
273
 
269
274
  response = self._client.beta.chat.completions.parse(**request)
270
275
 
271
- logger.debug(f"Full Response: {response}")
276
+ logger.debug(f"FullResponse: {response}")
272
277
 
273
278
  if response is None:
274
279
  logger.error("Received None response from OpenRouter API")
@@ -306,15 +311,20 @@ class OpenRouterClient:
306
311
  else:
307
312
  response.choices[0].message.parsed = content
308
313
  except Exception as e:
314
+ stack_trace = traceback.format_exc()
309
315
  logger.error(f"Failed to parse message content: {str(e)}")
310
- raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}")
316
+ logger.error(f"Stack trace:\n{stack_trace}")
317
+ raise OpenRouterAPIError(f"Failed to parse message content: {str(e)}\nStack trace:\n{stack_trace}")
311
318
 
312
319
  logger.info(f"Received response from OpenRouter: {len(response.choices)} choices")
313
320
 
314
321
  return response
315
322
 
316
323
  except Exception as e:
324
+ stack_trace = traceback.format_exc()
325
+ logger.debug(f"Raising error: {e}")
317
326
  logger.error(f"Error in parse completion: {str(e)}")
327
+ logger.error(f"Stack trace:\n{stack_trace}")
318
328
  logger.error(f"Request details: model={model or self.model}, response_format={response_format}, kwargs={kwargs}")
319
329
  logger.error(f"Message structure: {[{'role': msg.get('role'), 'content_length': len(str(msg.get('content', '')))} for msg in messages]}")
320
330
 
@@ -33,12 +33,12 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
33
33
 
34
34
  # Handle input wrapper
35
35
  if "properties" in schema and "input" in schema["properties"]:
36
- input_schema = schema["properties"]["input"]
36
+ inputSchema = schema["properties"]["input"]
37
37
 
38
38
  # If input has a $ref, resolve it
39
- if "$ref" in input_schema:
40
- ref = input_schema["$ref"].split("/")[-1]
41
- input_schema = schema.get("$defs", {}).get(ref, {})
39
+ if "$ref" in inputSchema:
40
+ ref = inputSchema["$ref"].split("/")[-1]
41
+ inputSchema = schema.get("$defs", {}).get(ref, {})
42
42
 
43
43
  if keep_input_wrapper:
44
44
  # Create the input property schema
@@ -50,8 +50,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
50
50
  }
51
51
 
52
52
  # Copy over input properties
53
- if "properties" in input_schema:
54
- for prop_name, prop in input_schema["properties"].items():
53
+ if "properties" in inputSchema:
54
+ for prop_name, prop in inputSchema["properties"].items():
55
55
  # Create a new property object with only allowed fields
56
56
  new_prop = {
57
57
  "type": prop.get("type", "string"),
@@ -65,8 +65,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
65
65
  input_prop_schema["properties"][prop_name] = new_prop
66
66
 
67
67
  # Copy over required fields for input schema
68
- if "required" in input_schema:
69
- input_prop_schema["required"] = input_schema["required"]
68
+ if "required" in inputSchema:
69
+ input_prop_schema["required"] = inputSchema["required"]
70
70
 
71
71
  # Add the input property to the main schema
72
72
  strict_schema["properties"]["input"] = input_prop_schema
@@ -76,8 +76,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
76
76
  strict_schema["required"] = schema["required"]
77
77
  else:
78
78
  # If not keeping input wrapper, use input schema directly
79
- if "properties" in input_schema:
80
- for prop_name, prop in input_schema["properties"].items():
79
+ if "properties" in inputSchema:
80
+ for prop_name, prop in inputSchema["properties"].items():
81
81
  # Create a new property object with only allowed fields
82
82
  new_prop = {
83
83
  "type": prop.get("type", "string"),
@@ -91,8 +91,8 @@ def convert_to_strict_schema(schema: dict[str, Any], strict: bool = True, keep_i
91
91
  strict_schema["properties"][prop_name] = new_prop
92
92
 
93
93
  # Copy over required fields
94
- if "required" in input_schema:
95
- strict_schema["required"] = input_schema["required"]
94
+ if "required" in inputSchema:
95
+ strict_schema["required"] = inputSchema["required"]
96
96
  else:
97
97
  # If no input wrapper, use the schema as is
98
98
  if "properties" in schema:
@@ -1,94 +0,0 @@
1
- """Example MCP server implementation."""
2
-
3
- from fastapi import FastAPI, HTTPException
4
- from pydantic import BaseModel
5
- import uvicorn
6
- from typing import Any
7
-
8
- app = FastAPI()
9
-
10
- class ScraperInput(BaseModel):
11
- url: str
12
-
13
- class ScrapeHtmlArguments(BaseModel):
14
- input: ScraperInput
15
-
16
-
17
- @app.get("/tools")
18
- async def get_tools():
19
- """Return the list of available tools."""
20
- return {
21
- "tools": [
22
- {
23
- "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 ",
24
- "input_schema": {
25
- "$defs": {
26
- "ScrapeHtmlInput": {
27
- "properties": {
28
- "url": {
29
- "description": "The URL to scrape HTML from",
30
- "minLength": 1,
31
- "title": "Url",
32
- "type": "string",
33
- }
34
- },
35
- "required": ["url"],
36
- "title": "ScrapeHtmlInput",
37
- "type": "object",
38
- }
39
- },
40
- "properties": {"input": {"$ref": "#/$defs/ScrapeHtmlInput"}},
41
- "required": ["input"],
42
- "title": "scrape_htmlArguments",
43
- "type": "object",
44
- },
45
- "internal_url": "http://localhost:8000/tools/scrape_html/invoke",
46
- "name": "scrape_html",
47
- "service": "html-structure-analyser",
48
- "strict": True,
49
- }
50
- ]
51
- }
52
-
53
-
54
- @app.post("/tools/scrape_html/invoke")
55
- async def scrape_html(arguments: ScrapeHtmlArguments):
56
- sample_html = """
57
- <!DOCTYPE html>
58
- <html lang="en">
59
- <head>
60
- <meta charset="UTF-8">
61
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
- <title>Sample Page</title>
63
- </head>
64
- <body>
65
- <header>
66
- <h1>Welcome to Sample Page</h1>
67
- <nav>
68
- <ul>
69
- <li><a href="#home">Home</a></li>
70
- <li><a href="#about">About</a></li>
71
- <li><a href="#contact">Contact</a></li>
72
- </ul>
73
- </nav>
74
- </header>
75
- <main>
76
- <section id="content">
77
- <h2>Main Content</h2>
78
- <p>This is a sample HTML page that was scraped from {arguments.input.url}</p>
79
- <article>
80
- <h3>Article Title</h3>
81
- <p>This is a sample article with some content.</p>
82
- </article>
83
- </section>
84
- </main>
85
- <footer>
86
- <p>&copy; 2024 Sample Website</p>
87
- </footer>
88
- </body>
89
- </html>
90
- """
91
- return sample_html
92
-
93
- if __name__ == "__main__":
94
- uvicorn.run(app, host="0.0.0.0", port=8000)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes