fast-agent-mcp 0.1.13__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
- fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
- mcp_agent/__init__.py +75 -0
- mcp_agent/agents/agent.py +59 -371
- mcp_agent/agents/base_agent.py +522 -0
- mcp_agent/agents/workflow/__init__.py +1 -0
- mcp_agent/agents/workflow/chain_agent.py +173 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
- mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
- mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +3 -1
- mcp_agent/cli/commands/bootstrap.py +18 -7
- mcp_agent/cli/commands/setup.py +12 -4
- mcp_agent/cli/main.py +1 -1
- mcp_agent/cli/terminal.py +1 -1
- mcp_agent/config.py +24 -35
- mcp_agent/context.py +3 -1
- mcp_agent/context_dependent.py +3 -1
- mcp_agent/core/agent_types.py +10 -7
- mcp_agent/core/direct_agent_app.py +179 -0
- mcp_agent/core/direct_decorators.py +443 -0
- mcp_agent/core/direct_factory.py +476 -0
- mcp_agent/core/enhanced_prompt.py +15 -20
- mcp_agent/core/fastagent.py +151 -337
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +19 -11
- mcp_agent/core/prompt.py +6 -2
- mcp_agent/core/validation.py +89 -16
- mcp_agent/executor/decorator_registry.py +6 -2
- mcp_agent/executor/temporal.py +35 -11
- mcp_agent/executor/workflow_signal.py +8 -2
- mcp_agent/human_input/handler.py +3 -1
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
- mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
- mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
- mcp_agent/logging/logger.py +2 -2
- mcp_agent/mcp/gen_client.py +9 -3
- mcp_agent/mcp/interfaces.py +67 -45
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +12 -4
- mcp_agent/mcp/mcp_agent_server.py +3 -1
- mcp_agent/mcp/mcp_aggregator.py +124 -93
- mcp_agent/mcp/mcp_connection_manager.py +21 -7
- mcp_agent/mcp/prompt_message_multipart.py +59 -1
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +20 -13
- mcp_agent/mcp/prompts/prompt_constants.py +18 -0
- mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
- mcp_agent/mcp/prompts/prompt_load.py +15 -5
- mcp_agent/mcp/prompts/prompt_server.py +154 -87
- mcp_agent/mcp/prompts/prompt_template.py +26 -35
- mcp_agent/mcp/resource_utils.py +3 -1
- mcp_agent/mcp/sampling.py +24 -15
- mcp_agent/mcp_server/agent_server.py +8 -5
- mcp_agent/mcp_server_registry.py +22 -9
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
- mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
- mcp_agent/resources/examples/internal/agent.py +4 -2
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/prompting/image_server.py +3 -1
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +27 -7
- fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
- mcp_agent/core/agent_app.py +0 -570
- mcp_agent/core/agent_utils.py +0 -69
- mcp_agent/core/decorators.py +0 -448
- mcp_agent/core/factory.py +0 -422
- mcp_agent/core/proxies.py +0 -278
- mcp_agent/core/types.py +0 -22
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -114
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
- mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
- mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
- mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
- mcp_agent/resources/examples/researcher/researcher.py +0 -39
- mcp_agent/resources/examples/workflows/chaining.py +0 -45
- mcp_agent/resources/examples/workflows/evaluator.py +0 -79
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -26
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
- mcp_agent/resources/examples/workflows/parallel.py +0 -79
- mcp_agent/resources/examples/workflows/router.py +0 -54
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -19
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -58
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -37
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -320
- mcp_agent/workflows/parallel/fan_out.py +0 -181
- mcp_agent/workflows/parallel/parallel_llm.py +0 -149
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -338
- mcp_agent/workflows/router/router_embedding.py +0 -226
- mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
- mcp_agent/workflows/router/router_embedding_openai.py +0 -59
- mcp_agent/workflows/router/router_llm.py +0 -304
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -292
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
- /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
- /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
@@ -0,0 +1,591 @@
|
|
1
|
+
"""
|
2
|
+
OrchestratorAgent implementation using the BaseAgent adapter pattern.
|
3
|
+
|
4
|
+
This workflow provides an implementation that manages complex tasks by
|
5
|
+
dynamically planning, delegating to specialized agents, and synthesizing results.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Dict, List, Literal, Optional, Type
|
9
|
+
|
10
|
+
from mcp.types import TextContent
|
11
|
+
|
12
|
+
from mcp_agent.agents.agent import Agent
|
13
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
14
|
+
from mcp_agent.agents.workflow.orchestrator_models import (
|
15
|
+
NextStep,
|
16
|
+
Plan,
|
17
|
+
PlanResult,
|
18
|
+
Step,
|
19
|
+
TaskWithResult,
|
20
|
+
format_plan_result,
|
21
|
+
format_step_result_text,
|
22
|
+
)
|
23
|
+
from mcp_agent.agents.workflow.orchestrator_prompts import (
|
24
|
+
FULL_PLAN_PROMPT_TEMPLATE,
|
25
|
+
ITERATIVE_PLAN_PROMPT_TEMPLATE,
|
26
|
+
SYNTHESIZE_INCOMPLETE_PLAN_TEMPLATE,
|
27
|
+
SYNTHESIZE_PLAN_PROMPT_TEMPLATE,
|
28
|
+
TASK_PROMPT_TEMPLATE,
|
29
|
+
)
|
30
|
+
from mcp_agent.core.agent_types import AgentConfig
|
31
|
+
from mcp_agent.core.exceptions import AgentConfigError
|
32
|
+
from mcp_agent.core.request_params import RequestParams
|
33
|
+
from mcp_agent.logging.logger import get_logger
|
34
|
+
from mcp_agent.mcp.interfaces import ModelT
|
35
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
36
|
+
|
37
|
+
logger = get_logger(__name__)
|
38
|
+
|
39
|
+
|
40
|
+
class OrchestratorAgent(BaseAgent):
|
41
|
+
"""
|
42
|
+
An agent that implements the orchestrator workflow pattern.
|
43
|
+
|
44
|
+
Dynamically creates execution plans and delegates tasks
|
45
|
+
to specialized worker agents, synthesizing their results into a cohesive output.
|
46
|
+
Supports both full planning and iterative planning modes.
|
47
|
+
"""
|
48
|
+
|
49
|
+
def __init__(
|
50
|
+
self,
|
51
|
+
config: AgentConfig,
|
52
|
+
agents: List[Agent],
|
53
|
+
plan_type: Literal["full", "iterative"] = "full",
|
54
|
+
context: Optional[Any] = None,
|
55
|
+
**kwargs,
|
56
|
+
) -> None:
|
57
|
+
"""
|
58
|
+
Initialize an OrchestratorAgent.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
config: Agent configuration or name
|
62
|
+
agents: List of specialized worker agents available for task execution
|
63
|
+
plan_type: Planning mode ("full" or "iterative")
|
64
|
+
context: Optional context object
|
65
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
66
|
+
"""
|
67
|
+
super().__init__(config, context=context, **kwargs)
|
68
|
+
|
69
|
+
if not agents:
|
70
|
+
raise AgentConfigError("At least one worker agent must be provided")
|
71
|
+
|
72
|
+
self.plan_type = plan_type
|
73
|
+
|
74
|
+
# Store agents by name for easier lookup
|
75
|
+
self.agents: Dict[str, Agent] = {}
|
76
|
+
for agent in agents:
|
77
|
+
agent_name = agent.name
|
78
|
+
self.logger.info(f"Adding agent '{agent_name}' to orchestrator")
|
79
|
+
self.agents[agent_name] = agent
|
80
|
+
|
81
|
+
# For tracking state during execution
|
82
|
+
self.plan_result: Optional[PlanResult] = None
|
83
|
+
|
84
|
+
async def generate(
|
85
|
+
self,
|
86
|
+
multipart_messages: List[PromptMessageMultipart],
|
87
|
+
request_params: Optional[RequestParams] = None,
|
88
|
+
) -> PromptMessageMultipart:
|
89
|
+
"""
|
90
|
+
Execute an orchestrated plan to process the input.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
multipart_messages: Messages to process
|
94
|
+
request_params: Optional request parameters
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
The final synthesized response from the orchestration
|
98
|
+
"""
|
99
|
+
# Extract user request
|
100
|
+
objective = multipart_messages[-1].all_text() if multipart_messages else ""
|
101
|
+
|
102
|
+
# Initialize execution parameters
|
103
|
+
params = self._merge_request_params(request_params)
|
104
|
+
|
105
|
+
# Execute the plan
|
106
|
+
plan_result = await self._execute_plan(objective, params)
|
107
|
+
self.plan_result = plan_result
|
108
|
+
|
109
|
+
# Return the result
|
110
|
+
return PromptMessageMultipart(
|
111
|
+
role="assistant",
|
112
|
+
content=[TextContent(type="text", text=plan_result.result or "No result available")],
|
113
|
+
)
|
114
|
+
|
115
|
+
async def structured(
|
116
|
+
self,
|
117
|
+
prompt: List[PromptMessageMultipart],
|
118
|
+
model: Type[ModelT],
|
119
|
+
request_params: Optional[RequestParams] = None,
|
120
|
+
) -> Optional[ModelT]:
|
121
|
+
"""
|
122
|
+
Execute an orchestration plan and parse the result into a structured format.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
prompt: List of messages to process
|
126
|
+
model: Pydantic model to parse the response into
|
127
|
+
request_params: Optional request parameters
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
The parsed final response, or None if parsing fails
|
131
|
+
"""
|
132
|
+
# Generate orchestration result
|
133
|
+
response = await self.generate(prompt, request_params)
|
134
|
+
|
135
|
+
# Try to parse the response into the specified model
|
136
|
+
try:
|
137
|
+
result_text = response.all_text()
|
138
|
+
prompt_message = PromptMessageMultipart(
|
139
|
+
role="user", content=[TextContent(type="text", text=result_text)]
|
140
|
+
)
|
141
|
+
|
142
|
+
# Use the LLM's structured parsing capability
|
143
|
+
return await self._llm.structured([prompt_message], model, request_params)
|
144
|
+
except Exception as e:
|
145
|
+
self.logger.warning(f"Failed to parse orchestration result: {str(e)}")
|
146
|
+
return None
|
147
|
+
|
148
|
+
async def initialize(self) -> None:
|
149
|
+
"""Initialize the orchestrator agent and worker agents."""
|
150
|
+
await super().initialize()
|
151
|
+
|
152
|
+
# Initialize all worker agents if not already initialized
|
153
|
+
for agent_name, agent in self.agents.items():
|
154
|
+
if not getattr(agent, "initialized", False):
|
155
|
+
self.logger.debug(f"Initializing agent: {agent_name}")
|
156
|
+
await agent.initialize()
|
157
|
+
|
158
|
+
self.initialized = True
|
159
|
+
|
160
|
+
async def shutdown(self) -> None:
|
161
|
+
"""Shutdown the orchestrator agent and worker agents."""
|
162
|
+
await super().shutdown()
|
163
|
+
|
164
|
+
# Shutdown all worker agents
|
165
|
+
for agent_name, agent in self.agents.items():
|
166
|
+
try:
|
167
|
+
await agent.shutdown()
|
168
|
+
except Exception as e:
|
169
|
+
self.logger.warning(f"Error shutting down agent {agent_name}: {str(e)}")
|
170
|
+
|
171
|
+
async def _execute_plan(self, objective: str, request_params: RequestParams) -> PlanResult:
|
172
|
+
"""
|
173
|
+
Execute a plan to achieve the given objective.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
objective: The objective to achieve
|
177
|
+
request_params: Request parameters for execution
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
PlanResult containing execution results and final output
|
181
|
+
"""
|
182
|
+
iterations = 0
|
183
|
+
total_steps_executed = 0
|
184
|
+
max_iterations = request_params.max_iterations
|
185
|
+
max_steps = getattr(request_params, "max_steps", max_iterations * 5)
|
186
|
+
|
187
|
+
# Initialize plan result
|
188
|
+
plan_result = PlanResult(objective=objective, step_results=[])
|
189
|
+
plan_result.max_iterations_reached = False
|
190
|
+
|
191
|
+
while iterations < max_iterations:
|
192
|
+
# Generate plan based on planning mode
|
193
|
+
if self.plan_type == "iterative":
|
194
|
+
next_step = await self._get_next_step(objective, plan_result, request_params)
|
195
|
+
if next_step is None:
|
196
|
+
self.logger.error("Failed to generate next step, ending iteration early")
|
197
|
+
plan_result.max_iterations_reached = True
|
198
|
+
break
|
199
|
+
|
200
|
+
logger.debug(f"Iteration {iterations}: Iterative plan:", data=next_step)
|
201
|
+
plan = Plan(steps=[next_step], is_complete=next_step.is_complete)
|
202
|
+
elif self.plan_type == "full":
|
203
|
+
plan = await self._get_full_plan(objective, plan_result, request_params)
|
204
|
+
if plan is None:
|
205
|
+
self.logger.error("Failed to generate full plan, ending iteration early")
|
206
|
+
plan_result.max_iterations_reached = True
|
207
|
+
break
|
208
|
+
|
209
|
+
logger.debug(f"Iteration {iterations}: Full Plan:", data=plan)
|
210
|
+
else:
|
211
|
+
raise ValueError(f"Invalid plan type: {self.plan_type}")
|
212
|
+
|
213
|
+
# Validate agent names early
|
214
|
+
self._validate_agent_names(plan)
|
215
|
+
|
216
|
+
# Store plan in result
|
217
|
+
plan_result.plan = plan
|
218
|
+
|
219
|
+
# Execute the steps in the plan
|
220
|
+
for step in plan.steps:
|
221
|
+
# Check if we've hit the step limit
|
222
|
+
if total_steps_executed >= max_steps:
|
223
|
+
self.logger.warning(
|
224
|
+
f"Reached maximum step limit ({max_steps}) without completing objective"
|
225
|
+
)
|
226
|
+
plan_result.max_steps_reached = True
|
227
|
+
break
|
228
|
+
|
229
|
+
# Execute the step and collect results
|
230
|
+
step_result = await self._execute_step(step, plan_result, request_params)
|
231
|
+
|
232
|
+
plan_result.add_step_result(step_result)
|
233
|
+
total_steps_executed += 1
|
234
|
+
|
235
|
+
# Check if we need to break due to hitting max steps
|
236
|
+
if getattr(plan_result, "max_steps_reached", False):
|
237
|
+
break
|
238
|
+
|
239
|
+
# If the plan is marked complete, finalize the result
|
240
|
+
if plan.is_complete:
|
241
|
+
plan_result.is_complete = True
|
242
|
+
break
|
243
|
+
|
244
|
+
# Increment iteration counter
|
245
|
+
iterations += 1
|
246
|
+
|
247
|
+
# Generate final result based on execution status
|
248
|
+
if iterations >= max_iterations and not plan_result.is_complete:
|
249
|
+
self.logger.warning(f"Failed to complete in {max_iterations} iterations")
|
250
|
+
plan_result.max_iterations_reached = True
|
251
|
+
|
252
|
+
# Use incomplete plan template
|
253
|
+
synthesis_prompt = SYNTHESIZE_INCOMPLETE_PLAN_TEMPLATE.format(
|
254
|
+
plan_result=format_plan_result(plan_result), max_iterations=max_iterations
|
255
|
+
)
|
256
|
+
else:
|
257
|
+
# Either plan is complete or we had other limits
|
258
|
+
if not plan_result.is_complete:
|
259
|
+
plan_result.is_complete = True
|
260
|
+
|
261
|
+
# Use standard template
|
262
|
+
synthesis_prompt = SYNTHESIZE_PLAN_PROMPT_TEMPLATE.format(
|
263
|
+
plan_result=format_plan_result(plan_result)
|
264
|
+
)
|
265
|
+
|
266
|
+
# Generate final synthesis
|
267
|
+
plan_result.result = await self._planner_generate_str(
|
268
|
+
synthesis_prompt, request_params.model_copy(update={"max_iterations": 1})
|
269
|
+
)
|
270
|
+
|
271
|
+
return plan_result
|
272
|
+
|
273
|
+
async def _execute_step(
|
274
|
+
self, step: Step, previous_result: PlanResult, request_params: RequestParams
|
275
|
+
) -> Any:
|
276
|
+
"""
|
277
|
+
Execute a single step from the plan.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
step: The step to execute
|
281
|
+
previous_result: Results of the plan execution so far
|
282
|
+
request_params: Request parameters
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
Result of executing the step
|
286
|
+
"""
|
287
|
+
from mcp_agent.agents.workflow.orchestrator_models import StepResult
|
288
|
+
|
289
|
+
# Initialize step result
|
290
|
+
step_result = StepResult(step=step, task_results=[])
|
291
|
+
|
292
|
+
# Format context for tasks
|
293
|
+
context = format_plan_result(previous_result)
|
294
|
+
|
295
|
+
# Execute all tasks in parallel
|
296
|
+
futures = []
|
297
|
+
error_tasks = []
|
298
|
+
|
299
|
+
for task in step.tasks:
|
300
|
+
# Check agent exists
|
301
|
+
agent = self.agents.get(task.agent)
|
302
|
+
if not agent:
|
303
|
+
self.logger.error(
|
304
|
+
f"No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
|
305
|
+
)
|
306
|
+
error_tasks.append(
|
307
|
+
(
|
308
|
+
task,
|
309
|
+
f"Error: Agent '{task.agent}' not found. Available agents: {', '.join(self.agents.keys())}",
|
310
|
+
)
|
311
|
+
)
|
312
|
+
continue
|
313
|
+
|
314
|
+
# Prepare task prompt
|
315
|
+
task_description = TASK_PROMPT_TEMPLATE.format(
|
316
|
+
objective=previous_result.objective, task=task.description, context=context
|
317
|
+
)
|
318
|
+
|
319
|
+
# Queue task for execution
|
320
|
+
futures.append(
|
321
|
+
(
|
322
|
+
task,
|
323
|
+
agent.generate(
|
324
|
+
[
|
325
|
+
PromptMessageMultipart(
|
326
|
+
role="user",
|
327
|
+
content=[TextContent(type="text", text=task_description)],
|
328
|
+
)
|
329
|
+
]
|
330
|
+
),
|
331
|
+
)
|
332
|
+
)
|
333
|
+
|
334
|
+
# Wait for all tasks
|
335
|
+
task_results = []
|
336
|
+
for future in futures:
|
337
|
+
task, future_obj = future
|
338
|
+
try:
|
339
|
+
result = await future_obj
|
340
|
+
result_text = result.all_text()
|
341
|
+
|
342
|
+
# Create task result
|
343
|
+
task_model = task.model_dump()
|
344
|
+
task_result = TaskWithResult(
|
345
|
+
description=task_model["description"],
|
346
|
+
agent=task_model["agent"],
|
347
|
+
result=result_text,
|
348
|
+
)
|
349
|
+
task_results.append(task_result)
|
350
|
+
except Exception as e:
|
351
|
+
self.logger.error(f"Error executing task: {str(e)}")
|
352
|
+
# Add error result
|
353
|
+
task_model = task.model_dump()
|
354
|
+
task_results.append(
|
355
|
+
TaskWithResult(
|
356
|
+
description=task_model["description"],
|
357
|
+
agent=task_model["agent"],
|
358
|
+
result=f"ERROR: {str(e)}",
|
359
|
+
)
|
360
|
+
)
|
361
|
+
|
362
|
+
# Add all task results to step result
|
363
|
+
for task_result in task_results:
|
364
|
+
step_result.add_task_result(task_result)
|
365
|
+
|
366
|
+
# Add error task results
|
367
|
+
for task, error_message in error_tasks:
|
368
|
+
task_model = task.model_dump()
|
369
|
+
step_result.add_task_result(
|
370
|
+
TaskWithResult(
|
371
|
+
description=task_model["description"],
|
372
|
+
agent=task_model["agent"],
|
373
|
+
result=f"ERROR: {error_message}",
|
374
|
+
)
|
375
|
+
)
|
376
|
+
|
377
|
+
# Format step result
|
378
|
+
step_result.result = format_step_result_text(step_result)
|
379
|
+
return step_result
|
380
|
+
|
381
|
+
async def _get_full_plan(
|
382
|
+
self, objective: str, plan_result: PlanResult, request_params: RequestParams
|
383
|
+
) -> Optional[Plan]:
|
384
|
+
"""
|
385
|
+
Generate a full plan with all steps.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
objective: The objective to achieve
|
389
|
+
plan_result: Current plan execution state
|
390
|
+
request_params: Request parameters
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
Complete Plan with all steps, or None if parsing fails
|
394
|
+
"""
|
395
|
+
# Format agent information for the prompt
|
396
|
+
agent_formats = []
|
397
|
+
for agent_name in self.agents.keys():
|
398
|
+
formatted = self._format_agent_info(agent_name)
|
399
|
+
agent_formats.append(formatted)
|
400
|
+
|
401
|
+
agents = "\n".join(agent_formats)
|
402
|
+
|
403
|
+
# Determine plan status
|
404
|
+
if plan_result.is_complete:
|
405
|
+
plan_status = "Plan Status: Complete"
|
406
|
+
elif plan_result.step_results:
|
407
|
+
plan_status = "Plan Status: In Progress"
|
408
|
+
else:
|
409
|
+
plan_status = "Plan Status: Not Started"
|
410
|
+
|
411
|
+
# Calculate iteration information
|
412
|
+
max_iterations = request_params.max_iterations
|
413
|
+
current_iteration = len(plan_result.step_results)
|
414
|
+
current_iteration = min(current_iteration, max_iterations - 1)
|
415
|
+
iterations_remaining = max(0, max_iterations - current_iteration - 1)
|
416
|
+
iterations_info = f"Planning Budget: Iteration {current_iteration + 1} of {max_iterations} (with {iterations_remaining} remaining)"
|
417
|
+
|
418
|
+
# Format the planning prompt
|
419
|
+
prompt = FULL_PLAN_PROMPT_TEMPLATE.format(
|
420
|
+
objective=objective,
|
421
|
+
plan_result=format_plan_result(plan_result),
|
422
|
+
plan_status=plan_status,
|
423
|
+
iterations_info=iterations_info,
|
424
|
+
agents=agents,
|
425
|
+
)
|
426
|
+
|
427
|
+
# Get structured response from LLM
|
428
|
+
try:
|
429
|
+
plan_msg = PromptMessageMultipart(
|
430
|
+
role="user", content=[TextContent(type="text", text=prompt)]
|
431
|
+
)
|
432
|
+
return await self._llm.structured([plan_msg], Plan, request_params)
|
433
|
+
except Exception as e:
|
434
|
+
self.logger.error(f"Failed to parse plan: {str(e)}")
|
435
|
+
return None
|
436
|
+
|
437
|
+
async def _get_next_step(
|
438
|
+
self, objective: str, plan_result: PlanResult, request_params: RequestParams
|
439
|
+
) -> Optional[NextStep]:
|
440
|
+
"""
|
441
|
+
Generate just the next step for iterative planning.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
objective: The objective to achieve
|
445
|
+
plan_result: Current plan execution state
|
446
|
+
request_params: Request parameters
|
447
|
+
|
448
|
+
Returns:
|
449
|
+
Next step to execute, or None if parsing fails
|
450
|
+
"""
|
451
|
+
# Format agent information
|
452
|
+
agents = "\n".join(
|
453
|
+
[self._format_agent_info(agent_name) for agent_name in self.agents.keys()]
|
454
|
+
)
|
455
|
+
|
456
|
+
# Determine plan status
|
457
|
+
if plan_result.is_complete:
|
458
|
+
plan_status = "Plan Status: Complete"
|
459
|
+
elif plan_result.step_results:
|
460
|
+
plan_status = "Plan Status: In Progress"
|
461
|
+
else:
|
462
|
+
plan_status = "Plan Status: Not Started"
|
463
|
+
|
464
|
+
# Calculate iteration information
|
465
|
+
max_iterations = request_params.max_iterations
|
466
|
+
current_iteration = len(plan_result.step_results)
|
467
|
+
iterations_remaining = max_iterations - current_iteration
|
468
|
+
iterations_info = (
|
469
|
+
f"Planning Budget: {iterations_remaining} of {max_iterations} iterations remaining"
|
470
|
+
)
|
471
|
+
|
472
|
+
# Format the planning prompt
|
473
|
+
prompt = ITERATIVE_PLAN_PROMPT_TEMPLATE.format(
|
474
|
+
objective=objective,
|
475
|
+
plan_result=format_plan_result(plan_result),
|
476
|
+
plan_status=plan_status,
|
477
|
+
iterations_info=iterations_info,
|
478
|
+
agents=agents,
|
479
|
+
)
|
480
|
+
|
481
|
+
# Get structured response from LLM
|
482
|
+
try:
|
483
|
+
plan_msg = PromptMessageMultipart(
|
484
|
+
role="user", content=[TextContent(type="text", text=prompt)]
|
485
|
+
)
|
486
|
+
return await self._llm.structured([plan_msg], NextStep, request_params)
|
487
|
+
except Exception as e:
|
488
|
+
self.logger.error(f"Failed to parse next step: {str(e)}")
|
489
|
+
return None
|
490
|
+
|
491
|
+
def _validate_agent_names(self, plan: Plan) -> None:
|
492
|
+
"""
|
493
|
+
Validate all agent names in a plan before execution.
|
494
|
+
|
495
|
+
Args:
|
496
|
+
plan: The plan to validate
|
497
|
+
"""
|
498
|
+
if plan is None:
|
499
|
+
self.logger.error("Cannot validate agent names: plan is None")
|
500
|
+
return
|
501
|
+
|
502
|
+
invalid_agents = []
|
503
|
+
|
504
|
+
for step in plan.steps:
|
505
|
+
for task in step.tasks:
|
506
|
+
if task.agent not in self.agents:
|
507
|
+
invalid_agents.append(task.agent)
|
508
|
+
|
509
|
+
if invalid_agents:
|
510
|
+
available_agents = ", ".join(self.agents.keys())
|
511
|
+
invalid_list = ", ".join(invalid_agents)
|
512
|
+
self.logger.error(
|
513
|
+
f"Plan contains invalid agent names: {invalid_list}. Available agents: {available_agents}"
|
514
|
+
)
|
515
|
+
|
516
|
+
def _format_agent_info(self, agent_name: str) -> str:
|
517
|
+
"""
|
518
|
+
Format agent information for display in prompts.
|
519
|
+
|
520
|
+
Args:
|
521
|
+
agent_name: Name of the agent to format
|
522
|
+
|
523
|
+
Returns:
|
524
|
+
Formatted agent information string
|
525
|
+
"""
|
526
|
+
agent = self.agents.get(agent_name)
|
527
|
+
if not agent:
|
528
|
+
self.logger.error(f"Agent '{agent_name}' not found in orchestrator agents")
|
529
|
+
return ""
|
530
|
+
|
531
|
+
# Get agent instruction or default description
|
532
|
+
instruction = (
|
533
|
+
agent.instruction if hasattr(agent, "instruction") else f"Agent '{agent_name}'"
|
534
|
+
)
|
535
|
+
|
536
|
+
# Format with XML tags
|
537
|
+
return f'<fastagent:agent name="{agent_name}">{instruction}</fastagent:agent>'
|
538
|
+
|
539
|
+
async def _planner_generate_str(self, message: str, request_params: RequestParams) -> str:
|
540
|
+
"""
|
541
|
+
Generate string response from the orchestrator's own LLM.
|
542
|
+
|
543
|
+
Args:
|
544
|
+
message: Message to send to the LLM
|
545
|
+
request_params: Request parameters
|
546
|
+
|
547
|
+
Returns:
|
548
|
+
String response from the LLM
|
549
|
+
"""
|
550
|
+
# Create prompt message
|
551
|
+
prompt = PromptMessageMultipart(
|
552
|
+
role="user", content=[TextContent(type="text", text=message)]
|
553
|
+
)
|
554
|
+
|
555
|
+
# Get response from LLM
|
556
|
+
response = await self._llm.generate([prompt], request_params)
|
557
|
+
return response.all_text()
|
558
|
+
|
559
|
+
def _merge_request_params(self, request_params: Optional[RequestParams]) -> RequestParams:
|
560
|
+
"""
|
561
|
+
Merge provided request parameters with defaults.
|
562
|
+
|
563
|
+
Args:
|
564
|
+
request_params: Optional request parameters to merge
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
Merged request parameters
|
568
|
+
"""
|
569
|
+
# Create orchestrator-specific defaults
|
570
|
+
defaults = RequestParams(
|
571
|
+
use_history=False, # Orchestrator doesn't use history
|
572
|
+
max_iterations=5, # Default to 5 iterations
|
573
|
+
maxTokens=8192, # Higher limit for planning
|
574
|
+
parallel_tool_calls=True,
|
575
|
+
)
|
576
|
+
|
577
|
+
# If base params provided, merge with defaults
|
578
|
+
if request_params:
|
579
|
+
# Create copy of defaults
|
580
|
+
params = defaults.model_copy()
|
581
|
+
# Update with provided params
|
582
|
+
if isinstance(request_params, dict):
|
583
|
+
params = params.model_copy(update=request_params)
|
584
|
+
else:
|
585
|
+
params = params.model_copy(update=request_params.model_dump())
|
586
|
+
|
587
|
+
# Force specific settings
|
588
|
+
params.use_history = False
|
589
|
+
return params
|
590
|
+
|
591
|
+
return defaults
|
@@ -2,7 +2,7 @@ from typing import List
|
|
2
2
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field
|
4
4
|
|
5
|
-
from mcp_agent.
|
5
|
+
from mcp_agent.agents.workflow.orchestrator_prompts import (
|
6
6
|
PLAN_RESULT_TEMPLATE,
|
7
7
|
STEP_RESULT_TEMPLATE,
|
8
8
|
TASK_RESULT_TEMPLATE,
|
@@ -67,7 +67,9 @@ class StepResult(BaseModel):
|
|
67
67
|
"""Result of executing a step"""
|
68
68
|
|
69
69
|
step: Step = Field(description="The step that was executed", default_factory=Step)
|
70
|
-
task_results: List[TaskWithResult] = Field(
|
70
|
+
task_results: List[TaskWithResult] = Field(
|
71
|
+
description="Results of executing each task", default_factory=list
|
72
|
+
)
|
71
73
|
result: str = Field(description="Result of executing the step", default="Step completed")
|
72
74
|
|
73
75
|
def add_task_result(self, task_result: TaskWithResult) -> None:
|
@@ -113,12 +115,16 @@ class NextStep(Step):
|
|
113
115
|
|
114
116
|
def format_task_result_text(task_result: TaskWithResult) -> str:
|
115
117
|
"""Format a task result as plain text for display"""
|
116
|
-
return TASK_RESULT_TEMPLATE.format(
|
118
|
+
return TASK_RESULT_TEMPLATE.format(
|
119
|
+
task_description=task_result.description, task_result=task_result.result
|
120
|
+
)
|
117
121
|
|
118
122
|
|
119
123
|
def format_step_result_text(step_result: StepResult) -> str:
|
120
124
|
"""Format a step result as plain text for display"""
|
121
|
-
tasks_str = "\n".join(
|
125
|
+
tasks_str = "\n".join(
|
126
|
+
f" - {format_task_result_text(task)}" for task in step_result.task_results
|
127
|
+
)
|
122
128
|
return STEP_RESULT_TEMPLATE.format(
|
123
129
|
step_description=step_result.step.description,
|
124
130
|
step_result=step_result.result,
|
@@ -129,7 +135,10 @@ def format_step_result_text(step_result: StepResult) -> str:
|
|
129
135
|
def format_plan_result_text(plan_result: PlanResult) -> str:
|
130
136
|
"""Format the full plan execution state as plain text for display"""
|
131
137
|
steps_str = (
|
132
|
-
"\n\n".join(
|
138
|
+
"\n\n".join(
|
139
|
+
f"{i + 1}:\n{format_step_result_text(step)}"
|
140
|
+
for i, step in enumerate(plan_result.step_results)
|
141
|
+
)
|
133
142
|
if plan_result.step_results
|
134
143
|
else "No steps executed yet"
|
135
144
|
)
|
@@ -143,18 +152,23 @@ def format_plan_result_text(plan_result: PlanResult) -> str:
|
|
143
152
|
|
144
153
|
def format_task_result_xml(task_result: TaskWithResult) -> str:
|
145
154
|
"""Format a task result with XML tags for better semantic understanding"""
|
146
|
-
from mcp_agent.
|
155
|
+
from mcp_agent.llm.prompt_utils import format_fastagent_tag
|
147
156
|
|
148
157
|
return format_fastagent_tag(
|
149
158
|
"task-result",
|
150
|
-
f"\n<fastagent:description>{task_result.description}</fastagent:description>\n"
|
151
|
-
|
159
|
+
f"\n<fastagent:description>{task_result.description}</fastagent:description>\n"
|
160
|
+
f"<fastagent:result>{task_result.result}</fastagent:result>\n",
|
161
|
+
{
|
162
|
+
"description": task_result.description[:50] + "..."
|
163
|
+
if len(task_result.description) > 50
|
164
|
+
else task_result.description
|
165
|
+
},
|
152
166
|
)
|
153
167
|
|
154
168
|
|
155
169
|
def format_step_result_xml(step_result: StepResult) -> str:
|
156
170
|
"""Format a step result with XML tags for better semantic understanding"""
|
157
|
-
from mcp_agent.
|
171
|
+
from mcp_agent.llm.prompt_utils import format_fastagent_tag
|
158
172
|
|
159
173
|
# Format each task result with XML
|
160
174
|
task_results = []
|
@@ -176,7 +190,7 @@ def format_step_result_xml(step_result: StepResult) -> str:
|
|
176
190
|
|
177
191
|
def format_plan_result(plan_result: PlanResult) -> str:
|
178
192
|
"""Format the full plan execution state with XML for better semantic understanding"""
|
179
|
-
from mcp_agent.
|
193
|
+
from mcp_agent.llm.prompt_utils import format_fastagent_tag
|
180
194
|
|
181
195
|
# Format objective
|
182
196
|
objective_tag = format_fastagent_tag("objective", plan_result.objective)
|
@@ -197,7 +211,9 @@ def format_plan_result(plan_result: PlanResult) -> str:
|
|
197
211
|
else:
|
198
212
|
# No steps executed yet
|
199
213
|
progress_content = (
|
200
|
-
f"{objective_tag}\n"
|
214
|
+
f"{objective_tag}\n"
|
215
|
+
f"<fastagent:steps>No steps executed yet</fastagent:steps>\n"
|
216
|
+
f"<fastagent:status>Not Started</fastagent:status>\n"
|
201
217
|
)
|
202
218
|
|
203
219
|
return format_fastagent_tag("progress", progress_content)
|