bizyengine 1.2.45__py3-none-any.whl → 1.2.71__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.
Files changed (40) hide show
  1. bizyengine/bizy_server/errno.py +21 -0
  2. bizyengine/bizy_server/server.py +130 -160
  3. bizyengine/bizy_server/utils.py +3 -0
  4. bizyengine/bizyair_extras/__init__.py +38 -31
  5. bizyengine/bizyair_extras/third_party_api/__init__.py +15 -0
  6. bizyengine/bizyair_extras/third_party_api/nodes_doubao.py +535 -0
  7. bizyengine/bizyair_extras/third_party_api/nodes_flux.py +173 -0
  8. bizyengine/bizyair_extras/third_party_api/nodes_gemini.py +403 -0
  9. bizyengine/bizyair_extras/third_party_api/nodes_gpt.py +101 -0
  10. bizyengine/bizyair_extras/third_party_api/nodes_hailuo.py +115 -0
  11. bizyengine/bizyair_extras/third_party_api/nodes_kling.py +404 -0
  12. bizyengine/bizyair_extras/third_party_api/nodes_sora.py +218 -0
  13. bizyengine/bizyair_extras/third_party_api/nodes_veo3.py +193 -0
  14. bizyengine/bizyair_extras/third_party_api/nodes_wan_api.py +198 -0
  15. bizyengine/bizyair_extras/third_party_api/trd_nodes_base.py +183 -0
  16. bizyengine/bizyair_extras/utils/aliyun_oss.py +92 -0
  17. bizyengine/bizyair_extras/utils/audio.py +88 -0
  18. bizyengine/bizybot/__init__.py +12 -0
  19. bizyengine/bizybot/client.py +774 -0
  20. bizyengine/bizybot/config.py +129 -0
  21. bizyengine/bizybot/coordinator.py +556 -0
  22. bizyengine/bizybot/exceptions.py +186 -0
  23. bizyengine/bizybot/mcp/__init__.py +3 -0
  24. bizyengine/bizybot/mcp/manager.py +520 -0
  25. bizyengine/bizybot/mcp/models.py +46 -0
  26. bizyengine/bizybot/mcp/registry.py +129 -0
  27. bizyengine/bizybot/mcp/routing.py +378 -0
  28. bizyengine/bizybot/models.py +344 -0
  29. bizyengine/core/__init__.py +1 -0
  30. bizyengine/core/commands/servers/prompt_server.py +10 -1
  31. bizyengine/core/common/client.py +8 -7
  32. bizyengine/core/common/utils.py +30 -1
  33. bizyengine/core/image_utils.py +12 -283
  34. bizyengine/misc/llm.py +32 -15
  35. bizyengine/misc/utils.py +179 -2
  36. bizyengine/version.txt +1 -1
  37. {bizyengine-1.2.45.dist-info → bizyengine-1.2.71.dist-info}/METADATA +3 -1
  38. {bizyengine-1.2.45.dist-info → bizyengine-1.2.71.dist-info}/RECORD +40 -16
  39. {bizyengine-1.2.45.dist-info → bizyengine-1.2.71.dist-info}/WHEEL +0 -0
  40. {bizyengine-1.2.45.dist-info → bizyengine-1.2.71.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ """
2
+ Data models for MCP client functionality
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+
9
+ @dataclass
10
+ class Tool:
11
+ """Represents an MCP tool"""
12
+
13
+ name: str
14
+ description: str
15
+ input_schema: Dict[str, Any]
16
+ server_name: str
17
+ title: Optional[str] = None
18
+
19
+ def to_openai_schema(self) -> Dict[str, Any]:
20
+ """Convert to OpenAI function calling format"""
21
+ from bizyengine.bizybot.mcp.routing import ToolFormatConverter
22
+
23
+ return ToolFormatConverter.mcp_to_openai(self)
24
+
25
+
26
+ @dataclass
27
+ class ServerStatus:
28
+ """Status information for an MCP server"""
29
+
30
+ name: str
31
+ connected: bool
32
+ session_id: Optional[str]
33
+ last_error: Optional[str]
34
+ capabilities: Dict[str, Any]
35
+ tools_count: int
36
+
37
+
38
+ @dataclass
39
+ class MCPSession:
40
+ """MCP session information"""
41
+
42
+ session_id: Optional[str]
43
+ protocol_version: str
44
+ server_capabilities: Dict[str, Any]
45
+ client_capabilities: Dict[str, Any]
46
+ initialized: bool = False
@@ -0,0 +1,129 @@
1
+ """
2
+ MCP tool registry for managing tools across multiple servers
3
+ """
4
+
5
+ from typing import Any, Dict, List, Tuple
6
+
7
+ from bizyengine.bizybot.mcp.models import Tool
8
+
9
+
10
+ class MCPToolRegistry:
11
+ """Registry for managing MCP tools across multiple servers"""
12
+
13
+ def __init__(self):
14
+ self.tools_by_name: Dict[str, Tuple[str, Tool]] = (
15
+ {}
16
+ ) # tool_name -> (server_name, tool)
17
+ self.tools_by_server: Dict[str, List[Tool]] = {} # server_name -> [tools]
18
+ self.tool_conflicts: Dict[str, List[str]] = (
19
+ {}
20
+ ) # tool_name -> [server_names] for conflicts
21
+
22
+ def register_server_tools(self, server_name: str, tools: List[Tool]) -> None:
23
+ """Register all tools from a specific server"""
24
+ # Clear existing tools for this server
25
+ self.unregister_server_tools(server_name)
26
+
27
+ # Register new tools
28
+ self.tools_by_server[server_name] = tools
29
+
30
+ for tool in tools:
31
+ if tool.name in self.tools_by_name:
32
+ # Handle tool name conflicts
33
+ existing_server = self.tools_by_name[tool.name][0]
34
+ if tool.name not in self.tool_conflicts:
35
+ self.tool_conflicts[tool.name] = [existing_server]
36
+ self.tool_conflicts[tool.name].append(server_name)
37
+
38
+ # For now, keep the first registered tool (could implement priority system)
39
+ continue
40
+
41
+ self.tools_by_name[tool.name] = (server_name, tool)
42
+
43
+ def unregister_server_tools(self, server_name: str) -> None:
44
+ """Unregister all tools from a specific server"""
45
+ if server_name not in self.tools_by_server:
46
+ return
47
+
48
+ # Remove tools from main registry
49
+ tools_to_remove = []
50
+ for tool_name, (srv_name, _) in self.tools_by_name.items():
51
+ if srv_name == server_name:
52
+ tools_to_remove.append(tool_name)
53
+
54
+ for tool_name in tools_to_remove:
55
+ del self.tools_by_name[tool_name]
56
+
57
+ # Clean up conflicts
58
+ if tool_name in self.tool_conflicts:
59
+ if server_name in self.tool_conflicts[tool_name]:
60
+ self.tool_conflicts[tool_name].remove(server_name)
61
+ if len(self.tool_conflicts[tool_name]) <= 1:
62
+ del self.tool_conflicts[tool_name]
63
+
64
+ # Remove server from tools_by_server
65
+ del self.tools_by_server[server_name]
66
+
67
+ def find_tool_server(self, tool_name: str) -> Tuple[str, Tool]:
68
+ """Find which server provides a specific tool"""
69
+ if tool_name not in self.tools_by_name:
70
+ available_tools = list(self.tools_by_name.keys())
71
+ raise ValueError(
72
+ f"Tool '{tool_name}' not found. Available tools: {available_tools}"
73
+ )
74
+
75
+ return self.tools_by_name[tool_name]
76
+
77
+ def get_all_tools(self) -> List[Tool]:
78
+ """Get all registered tools"""
79
+ return [tool for _, tool in self.tools_by_name.values()]
80
+
81
+ def get_server_tools(self, server_name: str) -> List[Tool]:
82
+ """Get all tools from a specific server"""
83
+ return self.tools_by_server.get(server_name, [])
84
+
85
+ def get_tools_for_llm(self) -> List[Dict[str, Any]]:
86
+ """Get all tools in OpenAI function calling format"""
87
+ tools = []
88
+ for tool in self.get_all_tools():
89
+ tools.append(tool.to_openai_schema())
90
+ return tools
91
+
92
+ def get_tool_conflicts(self) -> Dict[str, List[str]]:
93
+ """Get information about tool name conflicts"""
94
+ return self.tool_conflicts.copy()
95
+
96
+ def get_registry_stats(self) -> Dict[str, Any]:
97
+ """Get statistics about the tool registry"""
98
+ return {
99
+ "total_tools": len(self.tools_by_name),
100
+ "total_servers": len(self.tools_by_server),
101
+ "conflicts": len(self.tool_conflicts),
102
+ "tools_by_server": {
103
+ server: len(tools) for server, tools in self.tools_by_server.items()
104
+ },
105
+ }
106
+
107
+ def search_tools(self, query: str) -> List[Tuple[str, Tool]]:
108
+ """Search for tools by name or description"""
109
+ query_lower = query.lower()
110
+ results = []
111
+
112
+ for server_name, tool in self.tools_by_name.values():
113
+ if (
114
+ query_lower in tool.name.lower()
115
+ or query_lower in tool.description.lower()
116
+ ):
117
+ results.append((server_name, tool))
118
+
119
+ return results
120
+
121
+ def validate_tool_availability(self, tool_names: List[str]) -> Dict[str, bool]:
122
+ """Check if a list of tools are available"""
123
+ return {tool_name: tool_name in self.tools_by_name for tool_name in tool_names}
124
+
125
+ def clear(self) -> None:
126
+ """Clear all registered tools"""
127
+ self.tools_by_name.clear()
128
+ self.tools_by_server.clear()
129
+ self.tool_conflicts.clear()
@@ -0,0 +1,378 @@
1
+ """
2
+ MCP tool routing and format conversion utilities
3
+ """
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, List, Tuple
6
+
7
+ from bizyengine.bizybot.mcp.models import Tool
8
+
9
+ if TYPE_CHECKING:
10
+ # 仅用于类型检查,避免运行时循环依赖
11
+ from bizyengine.bizybot.mcp.manager import MCPClientManager
12
+
13
+
14
+ class ToolFormatConverter:
15
+ """Converts between MCP and OpenAI tool formats"""
16
+
17
+ @staticmethod
18
+ def mcp_to_openai(tool: Tool) -> Dict[str, Any]:
19
+ """Convert MCP tool to OpenAI function calling format"""
20
+ openai_tool = {
21
+ "type": "function",
22
+ "function": {
23
+ "name": tool.name,
24
+ "description": tool.description,
25
+ "parameters": tool.input_schema,
26
+ },
27
+ }
28
+
29
+ # Add title as description suffix if available and different from description
30
+ if tool.title and tool.title != tool.description:
31
+ openai_tool["function"]["description"] = f"{tool.title}: {tool.description}"
32
+
33
+ return openai_tool
34
+
35
+ @staticmethod
36
+ def mcp_tools_to_openai(tools: List[Tool]) -> List[Dict[str, Any]]:
37
+ """Convert list of MCP tools to OpenAI format"""
38
+ return [ToolFormatConverter.mcp_to_openai(tool) for tool in tools]
39
+
40
+ @staticmethod
41
+ def validate_openai_tool_call(tool_call: Dict[str, Any]) -> bool:
42
+ """Validate OpenAI tool call format"""
43
+ try:
44
+ required_fields = ["id", "type", "function"]
45
+ if not all(field in tool_call for field in required_fields):
46
+ return False
47
+
48
+ if tool_call["type"] != "function":
49
+ return False
50
+
51
+ function = tool_call["function"]
52
+ if not isinstance(function, dict):
53
+ return False
54
+
55
+ if "name" not in function or "arguments" not in function:
56
+ return False
57
+
58
+ return True
59
+
60
+ except Exception:
61
+ return False
62
+
63
+ @staticmethod
64
+ def extract_tool_call_info(
65
+ tool_call: Dict[str, Any],
66
+ ) -> Tuple[str, str, Dict[str, Any]]:
67
+ """Extract tool call information from OpenAI format"""
68
+ if not ToolFormatConverter.validate_openai_tool_call(tool_call):
69
+ raise ValueError("Invalid OpenAI tool call format")
70
+
71
+ call_id = tool_call["id"]
72
+ function = tool_call["function"]
73
+ tool_name = function["name"]
74
+
75
+ # Parse arguments (they come as JSON string)
76
+ import json
77
+
78
+ try:
79
+ arguments = json.loads(function["arguments"])
80
+ except json.JSONDecodeError as e:
81
+ raise ValueError(f"Invalid JSON in tool arguments: {e}")
82
+
83
+ return call_id, tool_name, arguments
84
+
85
+
86
+ class MCPToolRouter:
87
+ """Routes tool calls to appropriate MCP servers"""
88
+
89
+ def __init__(self, manager: "MCPClientManager"):
90
+ self.manager = manager
91
+
92
+ async def route_tool_call(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
93
+ """Route a tool call to the appropriate MCP server"""
94
+ try:
95
+ # Extract tool call information
96
+ call_id, tool_name, arguments = ToolFormatConverter.extract_tool_call_info(
97
+ tool_call
98
+ )
99
+
100
+ # Find the server that provides this tool
101
+ server_name, tool = self.manager.find_tool_server(tool_name)
102
+
103
+ # Call the tool on the appropriate server
104
+ result = await self.manager.call_tool_on_server(
105
+ server_name, tool_name, arguments
106
+ )
107
+
108
+ # Format the result for OpenAI
109
+ return self._format_tool_result(call_id, tool_name, server_name, result)
110
+
111
+ except Exception as e:
112
+ return self._format_tool_error(tool_call.get("id", "unknown"), str(e))
113
+
114
+ async def route_multiple_tool_calls(
115
+ self, tool_calls: List[Dict[str, Any]]
116
+ ) -> List[Dict[str, Any]]:
117
+ """Route multiple tool calls concurrently"""
118
+ import asyncio
119
+
120
+ # Execute all tool calls concurrently
121
+ tasks = [self.route_tool_call(tool_call) for tool_call in tool_calls]
122
+ results = await asyncio.gather(*tasks, return_exceptions=True)
123
+
124
+ # Process results and handle exceptions
125
+ formatted_results = []
126
+ for i, result in enumerate(results):
127
+ if isinstance(result, Exception):
128
+ call_id = tool_calls[i].get("id", f"unknown_{i}")
129
+ formatted_results.append(self._format_tool_error(call_id, str(result)))
130
+ else:
131
+ formatted_results.append(result)
132
+
133
+ return formatted_results
134
+
135
+ def _format_tool_result(
136
+ self, call_id: str, tool_name: str, server_name: str, result: Dict[str, Any]
137
+ ) -> Dict[str, Any]:
138
+ """Format MCP tool result for OpenAI"""
139
+ if result.get("isError", False):
140
+ return self._format_tool_error(
141
+ call_id,
142
+ f"Tool execution error: {result.get('content', 'Unknown error')}",
143
+ )
144
+
145
+ # Handle different result formats
146
+ formatted_content = ""
147
+
148
+ # Check if this is a standard MCP result with content array
149
+ if "content" in result and isinstance(result["content"], (list, str)):
150
+ # Standard MCP format
151
+ formatted_content = self._format_mcp_content(result["content"])
152
+ else:
153
+ # Direct result format (like from Exa API)
154
+ # Convert the entire result to a formatted string
155
+ formatted_content = self._format_direct_result(result)
156
+
157
+ formatted_result = {
158
+ "tool_call_id": call_id,
159
+ "role": "tool",
160
+ "content": formatted_content,
161
+ "name": tool_name,
162
+ # Additional metadata (not part of OpenAI spec but useful for debugging)
163
+ "_mcp_server": server_name,
164
+ "_mcp_raw_result": result,
165
+ "success": True,
166
+ }
167
+
168
+ return formatted_result
169
+
170
+ def _format_tool_error(self, call_id: str, error_message: str) -> Dict[str, Any]:
171
+ """Format tool error for OpenAI"""
172
+ return {
173
+ "tool_call_id": call_id,
174
+ "role": "tool",
175
+ "content": f"Error: {error_message}",
176
+ "_error": True,
177
+ }
178
+
179
+ def _format_mcp_content(self, content) -> str:
180
+ """Convert MCP content to string format"""
181
+ if not content:
182
+ return ""
183
+
184
+ # Handle different content formats
185
+ if isinstance(content, str):
186
+ return content
187
+
188
+ # Handle single content object (not in a list)
189
+ if hasattr(content, "text"):
190
+ # TextContent object
191
+ return content.text
192
+ elif hasattr(content, "type"):
193
+ # Single content dict
194
+ return self._format_single_content_item(content)
195
+
196
+ # Handle list of content items
197
+ if isinstance(content, list):
198
+ formatted_parts = []
199
+ for item in content:
200
+ if hasattr(item, "text"):
201
+ # TextContent object
202
+ formatted_parts.append(item.text)
203
+ elif isinstance(item, dict):
204
+ # Content dict
205
+ formatted_parts.append(self._format_single_content_item(item))
206
+ else:
207
+ # Unknown format, convert to string
208
+ formatted_parts.append(str(item))
209
+ return "\n".join(formatted_parts)
210
+
211
+ # Fallback: convert to string
212
+ return str(content)
213
+
214
+ def _format_single_content_item(self, item) -> str:
215
+ """Format a single content item (dict or object)"""
216
+ # Handle dict format
217
+ if isinstance(item, dict):
218
+ content_type = item.get("type", "unknown")
219
+
220
+ if content_type == "text":
221
+ text_content = item.get("text", "")
222
+ # Check if the text content is JSON that we should format nicely
223
+ return self._format_text_content(text_content)
224
+ elif content_type == "image":
225
+ # For images, include metadata but not the actual data
226
+ mime_type = item.get("mimeType", "unknown")
227
+ return f"[Image: {mime_type}]"
228
+ elif content_type == "resource":
229
+ # For resources, include the URI and title
230
+ resource = item.get("resource", {})
231
+ uri = resource.get("uri", "unknown")
232
+ title = resource.get("title", "")
233
+ return f"[Resource: {title or uri}]"
234
+ else:
235
+ # For unknown types, convert to string
236
+ return str(item)
237
+
238
+ # Handle object format (like TextContent)
239
+ if hasattr(item, "text"):
240
+ return self._format_text_content(item.text)
241
+ elif hasattr(item, "type"):
242
+ if item.type == "text" and hasattr(item, "text"):
243
+ return self._format_text_content(item.text)
244
+ else:
245
+ return f"[{item.type}]"
246
+
247
+ # Fallback
248
+ return str(item)
249
+
250
+ def _format_text_content(self, text_content: str) -> str:
251
+ """Format text content, handling JSON strings specially"""
252
+ try:
253
+ # Try to parse as JSON
254
+ import json
255
+
256
+ parsed_json = json.loads(text_content)
257
+
258
+ # If it's a JSON object, format it nicely
259
+ if isinstance(parsed_json, dict):
260
+ # Check if it looks like Exa search results
261
+ if "results" in parsed_json and isinstance(
262
+ parsed_json["results"], list
263
+ ):
264
+ return self._format_exa_search_results(parsed_json)
265
+ else:
266
+ # Format other JSON objects nicely
267
+ return json.dumps(parsed_json, indent=2, ensure_ascii=False)
268
+ else:
269
+ # If it's not a dict, just return the original text
270
+ return text_content
271
+
272
+ except (json.JSONDecodeError, TypeError):
273
+ # If it's not valid JSON, return as-is
274
+ return text_content
275
+
276
+ def _format_direct_result(self, result: Dict[str, Any]) -> str:
277
+ """Format direct JSON result (like from Exa API) into readable text"""
278
+ try:
279
+ # Handle Exa search results format
280
+ if "results" in result and isinstance(result["results"], list):
281
+ return self._format_exa_search_results(result)
282
+
283
+ # Handle other direct JSON formats
284
+ import json
285
+
286
+ return json.dumps(result, indent=2, ensure_ascii=False)
287
+
288
+ except Exception:
289
+ return str(result)
290
+
291
+ def _format_exa_search_results(self, result: Dict[str, Any]) -> str:
292
+ """Format Exa search results into readable text"""
293
+ try:
294
+ results = result.get("results", [])
295
+ search_info = []
296
+
297
+ # Add search metadata
298
+ if "autopromptString" in result:
299
+ search_info.append(f"Search Query: {result['autopromptString']}")
300
+
301
+ if "searchTime" in result:
302
+ search_info.append(f"Search Time: {result['searchTime']}ms")
303
+
304
+ if len(results) > 0:
305
+ search_info.append(f"Found {len(results)} results:")
306
+
307
+ # Format each result
308
+ formatted_results = []
309
+ for i, item in enumerate(results[:5], 1): # Limit to first 5 results
310
+ result_text = f"\n{i}. **{item.get('title', 'No title')}**"
311
+
312
+ if item.get("url"):
313
+ result_text += f"\n URL: {item['url']}"
314
+
315
+ if item.get("publishedDate"):
316
+ result_text += f"\n Published: {item['publishedDate']}"
317
+
318
+ if item.get("author"):
319
+ result_text += f"\n Author: {item['author']}"
320
+
321
+ # Add a snippet of the text content
322
+ if item.get("text"):
323
+ text_snippet = (
324
+ item["text"][:300] + "..."
325
+ if len(item["text"]) > 300
326
+ else item["text"]
327
+ )
328
+ result_text += f"\n Content: {text_snippet}"
329
+
330
+ formatted_results.append(result_text)
331
+
332
+ # Combine all parts
333
+ full_response = "\n".join(search_info)
334
+ if formatted_results:
335
+ full_response += "\n" + "\n".join(formatted_results)
336
+
337
+ # Add cost information if available
338
+ if "costDollars" in result:
339
+ cost = result["costDollars"].get("total", 0)
340
+ full_response += f"\n\nSearch cost: ${cost:.4f}"
341
+
342
+ return full_response
343
+
344
+ except Exception:
345
+ import json
346
+
347
+ return json.dumps(result, indent=2, ensure_ascii=False)
348
+
349
+
350
+ class ToolCallBatch:
351
+ """Manages a batch of tool calls for efficient processing"""
352
+
353
+ def __init__(self, tool_calls: List[Dict[str, Any]]):
354
+ self.tool_calls = tool_calls
355
+ self.results: List[Dict[str, Any]] = []
356
+ self.completed = False
357
+
358
+ async def execute(self, router: MCPToolRouter) -> List[Dict[str, Any]]:
359
+ """Execute all tool calls in the batch"""
360
+ if self.completed:
361
+ return self.results
362
+
363
+ self.results = await router.route_multiple_tool_calls(self.tool_calls)
364
+ self.completed = True
365
+
366
+ return self.results
367
+
368
+ def get_results_by_call_id(self) -> Dict[str, Dict[str, Any]]:
369
+ """Get results indexed by tool call ID"""
370
+ return {result["tool_call_id"]: result for result in self.results}
371
+
372
+ def get_successful_results(self) -> List[Dict[str, Any]]:
373
+ """Get only successful results"""
374
+ return [result for result in self.results if not result.get("_error", False)]
375
+
376
+ def get_error_results(self) -> List[Dict[str, Any]]:
377
+ """Get only error results"""
378
+ return [result for result in self.results if result.get("_error", False)]