hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.1__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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +449 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +121 -33
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +97 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/unified_search.py +689 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +99 -0
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +482 -0
- hanzo_mcp/tools/vector/infinity_store.py +731 -0
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +116 -0
- hanzo_mcp/tools/vector/vector_search.py +225 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
- hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -4,15 +4,19 @@ This module implements the AgentTool that allows Claude to delegate tasks to sub
|
|
|
4
4
|
enabling concurrent execution of multiple operations and specialized processing.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
7
8
|
import json
|
|
9
|
+
import re
|
|
8
10
|
import time
|
|
9
11
|
from collections.abc import Iterable
|
|
10
|
-
from typing import
|
|
12
|
+
from typing import Annotated, TypedDict, Unpack, final, override
|
|
11
13
|
|
|
12
14
|
import litellm
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
+
from fastmcp import Context as MCPContext
|
|
16
|
+
from fastmcp import FastMCP
|
|
17
|
+
from fastmcp.server.dependencies import get_context
|
|
15
18
|
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolParam
|
|
19
|
+
from pydantic import Field
|
|
16
20
|
|
|
17
21
|
from hanzo_mcp.tools.agent.prompt import (
|
|
18
22
|
get_allowed_agent_tools,
|
|
@@ -24,12 +28,32 @@ from hanzo_mcp.tools.agent.tool_adapter import (
|
|
|
24
28
|
convert_tools_to_openai_functions,
|
|
25
29
|
)
|
|
26
30
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
27
|
-
from hanzo_mcp.tools.common.
|
|
31
|
+
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
32
|
+
from hanzo_mcp.tools.common.context import (
|
|
33
|
+
ToolContext,
|
|
34
|
+
create_tool_context,
|
|
35
|
+
)
|
|
28
36
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
29
37
|
from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools
|
|
30
38
|
from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
|
|
40
|
+
Prompt = Annotated[
|
|
41
|
+
str,
|
|
42
|
+
Field(
|
|
43
|
+
description="Task for the agent to perform (must include absolute paths starting with /)",
|
|
44
|
+
min_length=1,
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentToolParams(TypedDict, total=False):
|
|
50
|
+
"""Parameters for the AgentTool.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
prompts: Task(s) for the agent to perform (must include absolute paths starting with /)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
prompts: str | list[str]
|
|
33
57
|
|
|
34
58
|
|
|
35
59
|
@final
|
|
@@ -58,96 +82,73 @@ class AgentTool(BaseTool):
|
|
|
58
82
|
Returns:
|
|
59
83
|
Tool description
|
|
60
84
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
This tool creates agents for delegation of tasks such as multi-step searches, complex analyses,
|
|
64
|
-
or other operations that benefit from focused processing. Multiple agents can work concurrently
|
|
65
|
-
on independent tasks, improving performance for complex operations.
|
|
85
|
+
# TODO: Add glob when it is implemented
|
|
86
|
+
at = [t.name for t in self.available_tools]
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
Results from all agents are combined in the final response.
|
|
88
|
+
return f"""Launch a new agent that has access to the following tools: {at} When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
When to use the Agent tool:
|
|
91
|
+
- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
""
|
|
93
|
+
When NOT to use the Agent tool:
|
|
94
|
+
- If you want to read a specific file path, use the read or glob tool instead of the Agent tool, to find the match more quickly
|
|
95
|
+
- If you are searching for a specific class definition like \"class Foo\", use the glob tool instead, to find the match more quickly
|
|
96
|
+
- If you are searching for code within a specific file or set of 2-3 files, use the read tool instead of the Agent tool, to find the match more quickly
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Parameter specifications
|
|
84
|
-
"""
|
|
85
|
-
return {
|
|
86
|
-
"properties": {
|
|
87
|
-
"prompts": {
|
|
88
|
-
"anyOf": [
|
|
89
|
-
{
|
|
90
|
-
"type": "array",
|
|
91
|
-
"items": {
|
|
92
|
-
"type": "string"
|
|
93
|
-
},
|
|
94
|
-
"description": "List of tasks for agents to perform concurrently"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"type": "string",
|
|
98
|
-
"description": "Single task for the agent to perform"
|
|
99
|
-
}
|
|
100
|
-
],
|
|
101
|
-
"description": "Task(s) for agent(s) to perform"
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
"required": ["prompts"],
|
|
105
|
-
"type": "object",
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
@property
|
|
109
|
-
@override
|
|
110
|
-
def required(self) -> list[str]:
|
|
111
|
-
"""Get the list of required parameter names.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
List of required parameter names
|
|
115
|
-
"""
|
|
116
|
-
return ["prompts"]
|
|
98
|
+
Usage notes:
|
|
99
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
100
|
+
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
101
|
+
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
102
|
+
4. The agent's outputs should generally be trusted
|
|
103
|
+
5.IMPORTANT: The Agent has no awareness of your context, so you must explicitly specify absolute project/file/directory paths and detailed background information about the current task. """
|
|
117
104
|
|
|
118
105
|
def __init__(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
self,
|
|
107
|
+
permission_manager: PermissionManager,
|
|
108
|
+
model: str | None = None,
|
|
109
|
+
api_key: str | None = None,
|
|
110
|
+
base_url: str | None = None,
|
|
111
|
+
max_tokens: int | None = None,
|
|
112
|
+
max_iterations: int = 10,
|
|
113
|
+
max_tool_uses: int = 30,
|
|
122
114
|
) -> None:
|
|
123
115
|
"""Initialize the agent tool.
|
|
124
116
|
|
|
125
117
|
Args:
|
|
126
|
-
|
|
118
|
+
|
|
127
119
|
permission_manager: Permission manager for access control
|
|
128
|
-
command_executor: Command executor for running shell commands
|
|
129
120
|
model: Optional model name override in LiteLLM format (e.g., "openai/gpt-4o")
|
|
130
121
|
api_key: Optional API key for the model provider
|
|
122
|
+
base_url: Optional base URL for the model provider API endpoint
|
|
131
123
|
max_tokens: Optional maximum tokens for model responses
|
|
132
124
|
max_iterations: Maximum number of iterations for agent (default: 10)
|
|
133
125
|
max_tool_uses: Maximum number of total tool uses for agent (default: 30)
|
|
134
126
|
"""
|
|
135
|
-
|
|
127
|
+
|
|
136
128
|
self.permission_manager = permission_manager
|
|
137
|
-
self.command_executor = command_executor
|
|
138
129
|
self.model_override = model
|
|
139
130
|
self.api_key_override = api_key
|
|
131
|
+
self.base_url_override = base_url
|
|
140
132
|
self.max_tokens_override = max_tokens
|
|
141
133
|
self.max_iterations = max_iterations
|
|
142
134
|
self.max_tool_uses = max_tool_uses
|
|
143
|
-
self.available_tools
|
|
144
|
-
self.available_tools.extend(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
self.available_tools: list[BaseTool] = []
|
|
136
|
+
self.available_tools.extend(
|
|
137
|
+
get_read_only_filesystem_tools(self.permission_manager)
|
|
138
|
+
)
|
|
139
|
+
self.available_tools.extend(
|
|
140
|
+
get_read_only_jupyter_tools(self.permission_manager)
|
|
141
|
+
)
|
|
142
|
+
self.available_tools.append(
|
|
143
|
+
BatchTool({t.name: t for t in self.available_tools})
|
|
144
|
+
)
|
|
148
145
|
|
|
149
146
|
@override
|
|
150
|
-
async def call(
|
|
147
|
+
async def call(
|
|
148
|
+
self,
|
|
149
|
+
ctx: MCPContext,
|
|
150
|
+
**params: Unpack[AgentToolParams],
|
|
151
|
+
) -> str:
|
|
151
152
|
"""Execute the tool with the given parameters.
|
|
152
153
|
|
|
153
154
|
Args:
|
|
@@ -158,132 +159,180 @@ Returns:
|
|
|
158
159
|
Tool execution result
|
|
159
160
|
"""
|
|
160
161
|
start_time = time.time()
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
# Create tool context
|
|
163
164
|
tool_ctx = create_tool_context(ctx)
|
|
164
|
-
tool_ctx.set_tool_info(self.name)
|
|
165
|
+
await tool_ctx.set_tool_info(self.name)
|
|
165
166
|
|
|
166
|
-
# Extract parameters
|
|
167
|
+
# Extract and validate parameters
|
|
167
168
|
prompts = params.get("prompts")
|
|
169
|
+
|
|
168
170
|
if prompts is None:
|
|
169
|
-
await tool_ctx.error("
|
|
170
|
-
return "Error:
|
|
171
|
+
await tool_ctx.error("No prompts provided")
|
|
172
|
+
return """Error: At least one prompt must be provided.
|
|
173
|
+
|
|
174
|
+
IMPORTANT REMINDER FOR CLAUDE:
|
|
175
|
+
The dispatch_agent tool requires prompts parameter. Please provide either:
|
|
176
|
+
- A single prompt as a string
|
|
177
|
+
- Multiple prompts as an array of strings
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
Each prompt must contain absolute paths starting with /.
|
|
180
|
+
Example of correct usage:
|
|
181
|
+
- prompts: "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
|
|
182
|
+
- prompts: ["Find files in /path/to/project", "Search code in /path/to/src"]"""
|
|
175
183
|
|
|
184
|
+
# Handle both string and list inputs
|
|
176
185
|
if isinstance(prompts, str):
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
186
|
+
prompt_list = [prompts]
|
|
187
|
+
elif isinstance(prompts, list):
|
|
188
|
+
if not prompts:
|
|
189
|
+
await tool_ctx.error("Empty prompts list provided")
|
|
190
|
+
return "Error: At least one prompt must be provided when using a list."
|
|
191
|
+
if not all(isinstance(p, str) for p in prompts):
|
|
192
|
+
await tool_ctx.error("All prompts must be strings")
|
|
193
|
+
return "Error: All prompts in the list must be strings."
|
|
194
|
+
prompt_list = prompts
|
|
195
|
+
else:
|
|
196
|
+
await tool_ctx.error("Invalid prompts parameter type")
|
|
197
|
+
return "Error: Parameter 'prompts' must be a string or an array of strings."
|
|
198
|
+
|
|
199
|
+
# Validate absolute paths in all prompts
|
|
200
|
+
absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
|
|
201
|
+
for prompt in prompt_list:
|
|
202
|
+
if not re.search(absolute_path_pattern, prompt):
|
|
203
|
+
await tool_ctx.error(f"Prompt does not contain absolute path: {prompt[:50]}...")
|
|
204
|
+
return """Error: All prompts must contain at least one absolute path.
|
|
205
|
+
|
|
206
|
+
IMPORTANT REMINDER FOR CLAUDE:
|
|
207
|
+
When using the dispatch_agent tool, always include absolute paths in your prompts.
|
|
208
|
+
Example of correct usage:
|
|
209
|
+
- "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
|
|
210
|
+
- "Find files that import the database module in /Users/bytedance/project/hanzo-mcp/src"
|
|
211
|
+
|
|
212
|
+
The agent cannot access files without knowing their absolute locations."""
|
|
213
|
+
|
|
214
|
+
# Execute agent(s) - always use _execute_multiple_agents for list inputs
|
|
215
|
+
if len(prompt_list) == 1:
|
|
216
|
+
await tool_ctx.info("Launching agent")
|
|
217
|
+
result = await self._execute_multiple_agents(prompt_list, tool_ctx)
|
|
218
|
+
execution_time = time.time() - start_time
|
|
219
|
+
formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
|
|
220
|
+
|
|
221
|
+
AGENT RESPONSE:
|
|
222
|
+
{result}"""
|
|
223
|
+
await tool_ctx.info(f"Agent execution completed in {execution_time:.2f}s")
|
|
224
|
+
return formatted_result
|
|
225
|
+
else:
|
|
226
|
+
await tool_ctx.info(f"Launching {len(prompt_list)} agents in parallel")
|
|
227
|
+
result = await self._execute_multiple_agents(prompt_list, tool_ctx)
|
|
228
|
+
execution_time = time.time() - start_time
|
|
229
|
+
formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(prompt_list)} agents).
|
|
230
|
+
|
|
231
|
+
AGENT RESPONSES:
|
|
232
|
+
{result}"""
|
|
233
|
+
await tool_ctx.info(f"Multi-agent execution completed in {execution_time:.2f}s")
|
|
234
|
+
return formatted_result
|
|
235
|
+
|
|
236
|
+
async def _execute_agent(self, prompt: str, tool_ctx: ToolContext) -> str:
|
|
237
|
+
"""Execute a single agent with the given prompt.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
prompt: The task prompt for the agent
|
|
241
|
+
tool_ctx: Tool context for logging
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Agent execution result
|
|
245
|
+
"""
|
|
246
|
+
# Get available tools for the agent
|
|
247
|
+
agent_tools = get_allowed_agent_tools(
|
|
248
|
+
self.available_tools,
|
|
249
|
+
self.permission_manager,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Convert tools to OpenAI format
|
|
253
|
+
openai_tools = convert_tools_to_openai_functions(agent_tools)
|
|
254
|
+
|
|
255
|
+
# Log execution start
|
|
256
|
+
await tool_ctx.info("Starting agent execution")
|
|
257
|
+
|
|
258
|
+
# Create a result container
|
|
259
|
+
result = ""
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Create system prompt for this agent
|
|
263
|
+
system_prompt = get_system_prompt(
|
|
264
|
+
agent_tools,
|
|
265
|
+
self.permission_manager,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Execute agent
|
|
269
|
+
await tool_ctx.info(f"Executing agent task: {prompt[:50]}...")
|
|
270
|
+
result = await self._execute_agent_with_tools(
|
|
271
|
+
system_prompt, prompt, agent_tools, openai_tools, tool_ctx
|
|
272
|
+
)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
# Log and return error result
|
|
275
|
+
error_message = f"Error executing agent: {str(e)}"
|
|
276
|
+
await tool_ctx.error(error_message)
|
|
277
|
+
return f"Error: {error_message}"
|
|
278
|
+
|
|
279
|
+
return result if result else "No results returned from agent"
|
|
202
280
|
|
|
203
281
|
async def _execute_multiple_agents(self, prompts: list[str], tool_ctx: ToolContext) -> str:
|
|
204
|
-
"""Execute multiple agents concurrently
|
|
282
|
+
"""Execute multiple agents concurrently.
|
|
205
283
|
|
|
206
284
|
Args:
|
|
207
285
|
prompts: List of prompts for the agents
|
|
208
286
|
tool_ctx: Tool context for logging
|
|
209
287
|
|
|
210
288
|
Returns:
|
|
211
|
-
Combined
|
|
289
|
+
Combined results from all agents
|
|
212
290
|
"""
|
|
213
|
-
# Get available tools for the agents
|
|
291
|
+
# Get available tools for the agents
|
|
214
292
|
agent_tools = get_allowed_agent_tools(
|
|
215
|
-
self.available_tools,
|
|
293
|
+
self.available_tools,
|
|
216
294
|
self.permission_manager,
|
|
217
295
|
)
|
|
218
|
-
|
|
219
|
-
# Convert tools to OpenAI format
|
|
296
|
+
|
|
297
|
+
# Convert tools to OpenAI format
|
|
220
298
|
openai_tools = convert_tools_to_openai_functions(agent_tools)
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
299
|
+
|
|
300
|
+
# Create system prompt for the agents
|
|
301
|
+
system_prompt = get_system_prompt(
|
|
302
|
+
agent_tools,
|
|
303
|
+
self.permission_manager,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Create tasks for parallel execution
|
|
226
307
|
tasks = []
|
|
227
|
-
results = []
|
|
228
|
-
|
|
229
|
-
# Handle exceptions for individual agent executions
|
|
230
308
|
for i, prompt in enumerate(prompts):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
system_prompt
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# Wait for all tasks to complete
|
|
259
|
-
completed_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
260
|
-
|
|
261
|
-
# Process results, handling any exceptions
|
|
262
|
-
for i, result in enumerate(completed_results):
|
|
263
|
-
if isinstance(result, Exception):
|
|
264
|
-
results.append(f"Agent {i+1} Error: {str(result)}")
|
|
265
|
-
else:
|
|
266
|
-
# For multi-agent case, add agent number prefix
|
|
267
|
-
if len(prompts) > 1:
|
|
268
|
-
results.append(f"Agent {i+1} Result:\n{result}")
|
|
269
|
-
else:
|
|
270
|
-
# For single agent case, just add the result
|
|
271
|
-
results.append(result)
|
|
272
|
-
except Exception as e:
|
|
273
|
-
# Handle any unexpected exceptions during gathering
|
|
274
|
-
error_message = f"Error executing agents concurrently: {str(e)}"
|
|
275
|
-
await tool_ctx.error(error_message)
|
|
276
|
-
results.append(f"Error: {error_message}")
|
|
277
|
-
|
|
278
|
-
# Combine results - different handling for single vs multi-agent
|
|
279
|
-
if len(prompts) > 1:
|
|
280
|
-
# Multi-agent: add separator between results
|
|
281
|
-
combined_result = "\n\n" + "\n\n---\n\n".join(results)
|
|
282
|
-
else:
|
|
283
|
-
# Single agent: just return the result
|
|
284
|
-
combined_result = results[0] if results else "No results returned from agent"
|
|
285
|
-
|
|
286
|
-
return combined_result
|
|
309
|
+
await tool_ctx.info(f"Creating agent task {i+1}: {prompt[:50]}...")
|
|
310
|
+
task = self._execute_agent_with_tools(
|
|
311
|
+
system_prompt, prompt, agent_tools, openai_tools, tool_ctx
|
|
312
|
+
)
|
|
313
|
+
tasks.append(task)
|
|
314
|
+
|
|
315
|
+
# Execute all agents concurrently
|
|
316
|
+
await tool_ctx.info(f"Executing {len(tasks)} agents in parallel")
|
|
317
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
318
|
+
|
|
319
|
+
# Handle single agent case
|
|
320
|
+
if len(results) == 1:
|
|
321
|
+
if isinstance(results[0], Exception):
|
|
322
|
+
await tool_ctx.error(f"Agent execution failed: {str(results[0])}")
|
|
323
|
+
return f"Error: {str(results[0])}"
|
|
324
|
+
return results[0]
|
|
325
|
+
|
|
326
|
+
# Format results for multiple agents
|
|
327
|
+
formatted_results = []
|
|
328
|
+
for i, result in enumerate(results):
|
|
329
|
+
if isinstance(result, Exception):
|
|
330
|
+
formatted_results.append(f"Agent {i+1} Error:\n{str(result)}")
|
|
331
|
+
await tool_ctx.error(f"Agent {i+1} failed: {str(result)}")
|
|
332
|
+
else:
|
|
333
|
+
formatted_results.append(f"Agent {i+1} Result:\n{result}")
|
|
334
|
+
|
|
335
|
+
return "\n\n---\n\n".join(formatted_results)
|
|
287
336
|
|
|
288
337
|
async def _execute_agent_with_tools(
|
|
289
338
|
self,
|
|
@@ -308,24 +357,26 @@ Returns:
|
|
|
308
357
|
# Get model parameters and name
|
|
309
358
|
model = get_default_model(self.model_override)
|
|
310
359
|
params = get_model_parameters(max_tokens=self.max_tokens_override)
|
|
311
|
-
|
|
360
|
+
|
|
312
361
|
# Initialize messages
|
|
313
|
-
messages:Iterable[ChatCompletionMessageParam] = []
|
|
362
|
+
messages: Iterable[ChatCompletionMessageParam] = []
|
|
314
363
|
messages.append({"role": "system", "content": system_prompt})
|
|
315
364
|
messages.append({"role": "user", "content": user_prompt})
|
|
316
|
-
|
|
365
|
+
|
|
317
366
|
# Track tool usage for metrics
|
|
318
367
|
tool_usage = {}
|
|
319
368
|
total_tool_use_count = 0
|
|
320
369
|
iteration_count = 0
|
|
321
370
|
max_tool_uses = self.max_tool_uses # Safety limit to prevent infinite loops
|
|
322
|
-
max_iterations =
|
|
371
|
+
max_iterations = (
|
|
372
|
+
self.max_iterations
|
|
373
|
+
) # Add a maximum number of iterations for safety
|
|
323
374
|
|
|
324
375
|
# Execute until the agent completes or reaches the limit
|
|
325
376
|
while total_tool_use_count < max_tool_uses and iteration_count < max_iterations:
|
|
326
377
|
iteration_count += 1
|
|
327
378
|
await tool_ctx.info(f"Calling model (iteration {iteration_count})...")
|
|
328
|
-
|
|
379
|
+
|
|
329
380
|
try:
|
|
330
381
|
# Configure model parameters based on capabilities
|
|
331
382
|
completion_params = {
|
|
@@ -339,88 +390,99 @@ Returns:
|
|
|
339
390
|
|
|
340
391
|
if self.api_key_override:
|
|
341
392
|
completion_params["api_key"] = self.api_key_override
|
|
342
|
-
|
|
393
|
+
|
|
343
394
|
# Add max_tokens if provided
|
|
344
395
|
if params.get("max_tokens"):
|
|
345
396
|
completion_params["max_tokens"] = params.get("max_tokens")
|
|
346
|
-
|
|
397
|
+
|
|
398
|
+
# Add base_url if provided
|
|
399
|
+
if self.base_url_override:
|
|
400
|
+
completion_params["base_url"] = self.base_url_override
|
|
401
|
+
|
|
347
402
|
# Make the model call
|
|
348
403
|
response = litellm.completion(
|
|
349
|
-
|
|
404
|
+
**completion_params # pyright: ignore
|
|
350
405
|
)
|
|
351
406
|
|
|
352
|
-
if len(response.choices) == 0:
|
|
407
|
+
if len(response.choices) == 0: # pyright: ignore
|
|
353
408
|
raise ValueError("No response choices returned")
|
|
354
409
|
|
|
355
|
-
message = response.choices[0].message
|
|
410
|
+
message = response.choices[0].message # pyright: ignore
|
|
356
411
|
|
|
357
412
|
# Add message to conversation history
|
|
358
|
-
messages.append(message)
|
|
413
|
+
messages.append(message) # pyright: ignore
|
|
359
414
|
|
|
360
415
|
# If no tool calls, we're done
|
|
361
416
|
if not message.tool_calls:
|
|
362
417
|
return message.content or "Agent completed with no response."
|
|
363
|
-
|
|
418
|
+
|
|
364
419
|
# Process tool calls
|
|
365
420
|
tool_call_count = len(message.tool_calls)
|
|
366
421
|
await tool_ctx.info(f"Processing {tool_call_count} tool calls")
|
|
367
|
-
|
|
422
|
+
|
|
368
423
|
for tool_call in message.tool_calls:
|
|
369
424
|
total_tool_use_count += 1
|
|
370
425
|
function_name = tool_call.function.name
|
|
371
|
-
|
|
426
|
+
|
|
372
427
|
# Track usage
|
|
373
428
|
tool_usage[function_name] = tool_usage.get(function_name, 0) + 1
|
|
374
|
-
|
|
429
|
+
|
|
375
430
|
# Log tool usage
|
|
376
431
|
await tool_ctx.info(f"Agent using tool: {function_name}")
|
|
377
|
-
|
|
432
|
+
|
|
378
433
|
# Parse the arguments
|
|
379
434
|
try:
|
|
380
435
|
function_args = json.loads(tool_call.function.arguments)
|
|
381
436
|
except json.JSONDecodeError:
|
|
382
437
|
function_args = {}
|
|
383
|
-
|
|
438
|
+
|
|
384
439
|
# Find the matching tool
|
|
385
|
-
tool = next(
|
|
440
|
+
tool = next(
|
|
441
|
+
(t for t in available_tools if t.name == function_name), None
|
|
442
|
+
)
|
|
386
443
|
if not tool:
|
|
387
444
|
tool_result = f"Error: Tool '{function_name}' not found"
|
|
388
445
|
else:
|
|
389
446
|
try:
|
|
390
|
-
tool_result = await tool.call(
|
|
447
|
+
tool_result = await tool.call(
|
|
448
|
+
ctx=tool_ctx.mcp_context, **function_args
|
|
449
|
+
)
|
|
391
450
|
except Exception as e:
|
|
392
451
|
tool_result = f"Error executing {function_name}: {str(e)}"
|
|
393
|
-
|
|
452
|
+
|
|
453
|
+
await tool_ctx.info(
|
|
454
|
+
f"tool {function_name} run with args {function_args} and return {tool_result[: min(100, len(tool_result))]}"
|
|
455
|
+
)
|
|
394
456
|
# Add the tool result to messages
|
|
395
457
|
messages.append(
|
|
396
458
|
{
|
|
397
459
|
"role": "tool",
|
|
398
460
|
"tool_call_id": tool_call.id,
|
|
461
|
+
"name": function_name,
|
|
399
462
|
"content": tool_result,
|
|
400
463
|
}
|
|
401
464
|
)
|
|
402
|
-
|
|
465
|
+
|
|
403
466
|
# Log progress
|
|
404
|
-
await tool_ctx.info(
|
|
405
|
-
|
|
467
|
+
await tool_ctx.info(
|
|
468
|
+
f"Processed {len(message.tool_calls)} tool calls. Total: {total_tool_use_count}"
|
|
469
|
+
)
|
|
470
|
+
|
|
406
471
|
except Exception as e:
|
|
407
472
|
await tool_ctx.error(f"Error in model call: {str(e)}")
|
|
408
473
|
# Avoid trying to JSON serialize message objects
|
|
409
474
|
await tool_ctx.error(f"Message count: {len(messages)}")
|
|
410
475
|
return f"Error in agent execution: {str(e)}"
|
|
411
|
-
|
|
476
|
+
|
|
412
477
|
# If we've reached the limit, add a warning and get final response
|
|
413
478
|
if total_tool_use_count >= max_tool_uses or iteration_count >= max_iterations:
|
|
414
|
-
limit_type = "tool usage" if total_tool_use_count >= max_tool_uses else "iterations"
|
|
415
|
-
await tool_ctx.info(f"Reached maximum {limit_type} limit. Getting final response.")
|
|
416
|
-
|
|
417
479
|
messages.append(
|
|
418
480
|
{
|
|
419
481
|
"role": "system",
|
|
420
|
-
"content":
|
|
482
|
+
"content": "You have reached the maximum iteration. Please provide your final response.",
|
|
421
483
|
}
|
|
422
484
|
)
|
|
423
|
-
|
|
485
|
+
|
|
424
486
|
try:
|
|
425
487
|
# Make a final call to get the result
|
|
426
488
|
final_response = litellm.completion(
|
|
@@ -430,45 +492,49 @@ Returns:
|
|
|
430
492
|
timeout=params["timeout"],
|
|
431
493
|
max_tokens=params.get("max_tokens"),
|
|
432
494
|
)
|
|
433
|
-
|
|
434
|
-
return
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
final_response.choices[0].message.content
|
|
498
|
+
or "Agent reached max iteration limit without a response."
|
|
499
|
+
) # pyright: ignore
|
|
435
500
|
except Exception as e:
|
|
436
501
|
await tool_ctx.error(f"Error in final model call: {str(e)}")
|
|
437
502
|
return f"Error in final response: {str(e)}"
|
|
438
|
-
|
|
503
|
+
|
|
439
504
|
# Should not reach here but just in case
|
|
440
505
|
return "Agent execution completed after maximum iterations."
|
|
441
506
|
|
|
442
|
-
def _format_result(self, result: str, execution_time: float
|
|
507
|
+
def _format_result(self, result: str, execution_time: float) -> str:
|
|
443
508
|
"""Format agent result with metrics.
|
|
444
509
|
|
|
445
510
|
Args:
|
|
446
|
-
result: Raw result from agent
|
|
511
|
+
result: Raw result from agent
|
|
447
512
|
execution_time: Execution time in seconds
|
|
448
|
-
agent_count: Number of agents used
|
|
449
513
|
|
|
450
514
|
Returns:
|
|
451
515
|
Formatted result with metrics
|
|
452
516
|
"""
|
|
453
|
-
|
|
454
|
-
if agent_count > 1:
|
|
455
|
-
# Multi-agent response
|
|
456
|
-
return f"""Multi-agent execution completed in {execution_time:.2f} seconds ({agent_count} agents).
|
|
457
|
-
|
|
458
|
-
{result}
|
|
459
|
-
"""
|
|
460
|
-
else:
|
|
461
|
-
# Single agent response
|
|
462
|
-
return f"""Agent execution completed in {execution_time:.2f} seconds.
|
|
517
|
+
return f"""Agent execution completed in {execution_time:.2f} seconds.
|
|
463
518
|
|
|
464
519
|
AGENT RESPONSE:
|
|
465
520
|
{result}
|
|
466
521
|
"""
|
|
467
|
-
|
|
522
|
+
|
|
468
523
|
@override
|
|
469
524
|
def register(self, mcp_server: FastMCP) -> None:
|
|
525
|
+
"""Register this agent tool with the MCP server.
|
|
526
|
+
|
|
527
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
528
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
mcp_server: The FastMCP server instance
|
|
532
|
+
"""
|
|
470
533
|
tool_self = self # Create a reference to self for use in the closure
|
|
471
|
-
|
|
472
|
-
@mcp_server.tool(name=self.name, description=self.
|
|
473
|
-
async def dispatch_agent(
|
|
474
|
-
|
|
534
|
+
|
|
535
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
536
|
+
async def dispatch_agent(
|
|
537
|
+
prompts: str | list[str],
|
|
538
|
+
) -> str:
|
|
539
|
+
ctx = get_context()
|
|
540
|
+
return await tool_self.call(ctx, prompts=prompts)
|