bizyengine 1.2.49__py3-none-any.whl → 1.2.51__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.
- bizyengine/bizy_server/errno.py +21 -0
- bizyengine/bizy_server/server.py +119 -159
- bizyengine/bizyair_extras/nodes_gemini.py +211 -62
- bizyengine/bizybot/__init__.py +12 -0
- bizyengine/bizybot/client.py +774 -0
- bizyengine/bizybot/config.py +129 -0
- bizyengine/bizybot/coordinator.py +556 -0
- bizyengine/bizybot/exceptions.py +186 -0
- bizyengine/bizybot/mcp/__init__.py +3 -0
- bizyengine/bizybot/mcp/manager.py +520 -0
- bizyengine/bizybot/mcp/models.py +46 -0
- bizyengine/bizybot/mcp/registry.py +129 -0
- bizyengine/bizybot/mcp/routing.py +378 -0
- bizyengine/bizybot/models.py +344 -0
- bizyengine/core/common/client.py +0 -1
- bizyengine/version.txt +1 -1
- {bizyengine-1.2.49.dist-info → bizyengine-1.2.51.dist-info}/METADATA +2 -1
- {bizyengine-1.2.49.dist-info → bizyengine-1.2.51.dist-info}/RECORD +20 -9
- {bizyengine-1.2.49.dist-info → bizyengine-1.2.51.dist-info}/WHEEL +0 -0
- {bizyengine-1.2.49.dist-info → bizyengine-1.2.51.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)]
|