fast-agent-mcp 0.0.15__py3-none-any.whl → 0.0.16__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.0.15.dist-info → fast_agent_mcp-0.0.16.dist-info}/METADATA +116 -17
- {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.0.16.dist-info}/RECORD +20 -19
- mcp_agent/cli/__main__.py +3 -0
- mcp_agent/config.py +19 -11
- mcp_agent/core/enhanced_prompt.py +10 -2
- mcp_agent/resources/examples/data-analysis/analysis.py +34 -8
- mcp_agent/resources/examples/workflows/evaluator.py +0 -2
- mcp_agent/resources/examples/workflows/parallel.py +0 -4
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +112 -31
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +16 -2
- mcp_agent/workflows/llm/augmented_llm_openai.py +13 -1
- mcp_agent/workflows/llm/prompt_utils.py +137 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +206 -52
- mcp_agent/workflows/orchestrator/orchestrator_models.py +81 -9
- mcp_agent/workflows/orchestrator/orchestrator_prompts.py +112 -42
- mcp_agent/workflows/router/router_base.py +113 -21
- mcp_agent/workflows/router/router_llm.py +19 -5
- {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.0.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.0.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.0.16.dist-info}/licenses/LICENSE +0 -0
@@ -117,10 +117,11 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
117
117
|
responses: List[Message] = []
|
118
118
|
model = await self.select_model(params)
|
119
119
|
chat_turn = (len(messages) + 1) // 2
|
120
|
-
self._log_chat_progress(chat_turn, model=model)
|
121
120
|
self.show_user_message(str(message), model, chat_turn)
|
122
121
|
|
123
122
|
for i in range(params.max_iterations):
|
123
|
+
chat_turn = (len(messages) + 1) // 2
|
124
|
+
self._log_chat_progress(chat_turn, model=model)
|
124
125
|
arguments = {
|
125
126
|
"model": model,
|
126
127
|
"messages": messages,
|
@@ -208,10 +209,23 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
|
|
208
209
|
break
|
209
210
|
elif response.stop_reason == "max_tokens":
|
210
211
|
# We have reached the max tokens limit
|
212
|
+
|
211
213
|
self.logger.debug(
|
212
214
|
f"Iteration {i}: Stopping because finish_reason is 'max_tokens'"
|
213
215
|
)
|
214
|
-
|
216
|
+
if params.maxTokens is not None:
|
217
|
+
message_text = Text(
|
218
|
+
f"the assistant has reached the maximum token limit ({params.maxTokens})",
|
219
|
+
style="dim green italic",
|
220
|
+
)
|
221
|
+
else:
|
222
|
+
message_text = Text(
|
223
|
+
"the assistant has reached the maximum token limit",
|
224
|
+
style="dim green italic",
|
225
|
+
)
|
226
|
+
|
227
|
+
await self.show_assistant_message(message_text)
|
228
|
+
|
215
229
|
break
|
216
230
|
else:
|
217
231
|
message_text = ""
|
@@ -254,7 +254,7 @@ class OpenAIAugmentedLLM(
|
|
254
254
|
message_text,
|
255
255
|
message.tool_calls[
|
256
256
|
0
|
257
|
-
].function.name, # TODO support multiple tool calls
|
257
|
+
].function.name, # TODO support displaying multiple tool calls
|
258
258
|
)
|
259
259
|
else:
|
260
260
|
await self.show_assistant_message(
|
@@ -294,6 +294,18 @@ class OpenAIAugmentedLLM(
|
|
294
294
|
self.logger.debug(
|
295
295
|
f"Iteration {i}: Stopping because finish_reason is 'length'"
|
296
296
|
)
|
297
|
+
if request_params and request_params.maxTokens is not None:
|
298
|
+
message_text = Text(
|
299
|
+
f"the assistant has reached the maximum token limit ({request_params.maxTokens})",
|
300
|
+
style="dim green italic",
|
301
|
+
)
|
302
|
+
else:
|
303
|
+
message_text = Text(
|
304
|
+
"the assistant has reached the maximum token limit",
|
305
|
+
style="dim green italic",
|
306
|
+
)
|
307
|
+
|
308
|
+
await self.show_assistant_message(message_text)
|
297
309
|
# TODO: saqadri - would be useful to return the reason for stopping to the caller
|
298
310
|
break
|
299
311
|
elif choice.finish_reason == "content_filter":
|
@@ -0,0 +1,137 @@
|
|
1
|
+
"""
|
2
|
+
XML formatting utilities for consistent prompt engineering across components.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Dict, List, Optional, Union
|
6
|
+
|
7
|
+
|
8
|
+
def format_xml_tag(tag_name: str, content: Optional[str] = None,
|
9
|
+
attributes: Optional[Dict[str, str]] = None) -> str:
|
10
|
+
"""
|
11
|
+
Format an XML tag with optional content and attributes.
|
12
|
+
Uses self-closing tag when content is None or empty.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
tag_name: Name of the XML tag
|
16
|
+
content: Content to include inside the tag (None for self-closing)
|
17
|
+
attributes: Dictionary of attribute name-value pairs
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Formatted XML tag as string
|
21
|
+
"""
|
22
|
+
# Format attributes if provided
|
23
|
+
attrs_str = ""
|
24
|
+
if attributes:
|
25
|
+
attrs_str = " " + " ".join(f'{k}="{v}"' for k, v in attributes.items())
|
26
|
+
|
27
|
+
# Use self-closing tag if no content
|
28
|
+
if content is None or content == "":
|
29
|
+
return f"<{tag_name}{attrs_str} />"
|
30
|
+
|
31
|
+
# Full tag with content
|
32
|
+
return f"<{tag_name}{attrs_str}>{content}</{tag_name}>"
|
33
|
+
|
34
|
+
|
35
|
+
def format_fastagent_tag(tag_type: str, content: Optional[str] = None,
|
36
|
+
attributes: Optional[Dict[str, str]] = None) -> str:
|
37
|
+
"""
|
38
|
+
Format a fastagent-namespaced XML tag with consistent formatting.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
tag_type: Type of fastagent tag (without namespace prefix)
|
42
|
+
content: Content to include inside the tag
|
43
|
+
attributes: Dictionary of attribute name-value pairs
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Formatted fastagent XML tag as string
|
47
|
+
"""
|
48
|
+
return format_xml_tag(f"fastagent:{tag_type}", content, attributes)
|
49
|
+
|
50
|
+
|
51
|
+
def format_server_info(server_name: str, description: Optional[str] = None,
|
52
|
+
tools: Optional[List[Dict[str, str]]] = None) -> str:
|
53
|
+
"""
|
54
|
+
Format server information consistently across router and orchestrator modules.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
server_name: Name of the server
|
58
|
+
description: Optional server description
|
59
|
+
tools: Optional list of tool dictionaries with 'name' and 'description' keys
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Formatted server XML as string
|
63
|
+
"""
|
64
|
+
# Use self-closing tag if no description or tools
|
65
|
+
if not description and not tools:
|
66
|
+
return format_fastagent_tag("server", None, {"name": server_name})
|
67
|
+
|
68
|
+
# Start building components
|
69
|
+
components = []
|
70
|
+
|
71
|
+
# Add description if present
|
72
|
+
if description:
|
73
|
+
desc_tag = format_fastagent_tag("description", description)
|
74
|
+
components.append(desc_tag)
|
75
|
+
|
76
|
+
# Add tools section if tools exist
|
77
|
+
if tools and len(tools) > 0:
|
78
|
+
tool_tags = []
|
79
|
+
for tool in tools:
|
80
|
+
tool_name = tool.get("name", "")
|
81
|
+
tool_desc = tool.get("description", "")
|
82
|
+
tool_tag = format_fastagent_tag("tool", tool_desc, {"name": tool_name})
|
83
|
+
tool_tags.append(tool_tag)
|
84
|
+
|
85
|
+
tools_content = "\n".join(tool_tags)
|
86
|
+
tools_tag = format_fastagent_tag("tools", f"\n{tools_content}\n")
|
87
|
+
components.append(tools_tag)
|
88
|
+
|
89
|
+
# Combine all components
|
90
|
+
server_content = "\n".join(components)
|
91
|
+
return format_fastagent_tag("server", f"\n{server_content}\n", {"name": server_name})
|
92
|
+
|
93
|
+
|
94
|
+
def format_agent_info(agent_name: str, description: Optional[str] = None,
|
95
|
+
servers: Optional[List[Dict[str, Union[str, List[Dict[str, str]]]]]] = None) -> str:
|
96
|
+
"""
|
97
|
+
Format agent information consistently across router and orchestrator modules.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
agent_name: Name of the agent
|
101
|
+
description: Optional agent description/instruction
|
102
|
+
servers: Optional list of server dictionaries with 'name', 'description', and 'tools' keys
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Formatted agent XML as string
|
106
|
+
"""
|
107
|
+
# Start building components
|
108
|
+
components = []
|
109
|
+
|
110
|
+
# Add description if present
|
111
|
+
if description:
|
112
|
+
desc_tag = format_fastagent_tag("description", description)
|
113
|
+
components.append(desc_tag)
|
114
|
+
|
115
|
+
# If no description or servers, use self-closing tag
|
116
|
+
if not description and not servers:
|
117
|
+
return format_fastagent_tag("agent", None, {"name": agent_name})
|
118
|
+
|
119
|
+
# If has servers, format them
|
120
|
+
if servers and len(servers) > 0:
|
121
|
+
server_tags = []
|
122
|
+
for server in servers:
|
123
|
+
server_name = server.get("name", "")
|
124
|
+
server_desc = server.get("description", "")
|
125
|
+
server_tools = server.get("tools", [])
|
126
|
+
server_tag = format_server_info(server_name, server_desc, server_tools)
|
127
|
+
server_tags.append(server_tag)
|
128
|
+
|
129
|
+
# Only add servers section if we have servers
|
130
|
+
if server_tags:
|
131
|
+
servers_content = "\n".join(server_tags)
|
132
|
+
servers_tag = format_fastagent_tag("servers", f"\n{servers_content}\n")
|
133
|
+
components.append(servers_tag)
|
134
|
+
|
135
|
+
# Combine all components
|
136
|
+
agent_content = "\n".join(components)
|
137
|
+
return format_fastagent_tag("agent", f"\n{agent_content}\n", {"name": agent_name})
|
@@ -21,7 +21,7 @@ from mcp_agent.workflows.llm.augmented_llm import (
|
|
21
21
|
)
|
22
22
|
from mcp_agent.workflows.orchestrator.orchestrator_models import (
|
23
23
|
format_plan_result,
|
24
|
-
|
24
|
+
format_step_result_text,
|
25
25
|
NextStep,
|
26
26
|
Plan,
|
27
27
|
PlanResult,
|
@@ -163,15 +163,27 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
163
163
|
request_params: RequestParams | None = None,
|
164
164
|
) -> ModelT:
|
165
165
|
"""Request a structured LLM generation and return the result as a Pydantic model."""
|
166
|
+
import json
|
167
|
+
from pydantic import ValidationError
|
168
|
+
|
166
169
|
params = self.get_request_params(request_params)
|
167
170
|
result_str = await self.generate_str(message=message, request_params=params)
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
response_model
|
173
|
-
|
174
|
-
|
171
|
+
|
172
|
+
try:
|
173
|
+
# Directly parse JSON and create model instance
|
174
|
+
parsed_data = json.loads(result_str)
|
175
|
+
return response_model(**parsed_data)
|
176
|
+
except (json.JSONDecodeError, ValidationError) as e:
|
177
|
+
# Log the error and fall back to the original method if direct parsing fails
|
178
|
+
self.logger.error(f"Direct JSON parsing failed: {str(e)}. Falling back to standard method.")
|
179
|
+
self.logger.debug(f"Failed JSON content: {result_str}")
|
180
|
+
|
181
|
+
# Use AugmentedLLM's structured output handling as fallback
|
182
|
+
return await super().generate_structured(
|
183
|
+
message=result_str,
|
184
|
+
response_model=response_model,
|
185
|
+
request_params=params,
|
186
|
+
)
|
175
187
|
|
176
188
|
async def execute(
|
177
189
|
self, objective: str, request_params: RequestParams | None = None
|
@@ -205,11 +217,15 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
205
217
|
)
|
206
218
|
logger.debug(f"Iteration {iterations}: Iterative plan:", data=next_step)
|
207
219
|
plan = Plan(steps=[next_step], is_complete=next_step.is_complete)
|
220
|
+
# Validate agent names in the plan early
|
221
|
+
self._validate_agent_names(plan)
|
208
222
|
elif self.plan_type == "full":
|
209
223
|
plan = await self._get_full_plan(
|
210
224
|
objective=objective, plan_result=plan_result, request_params=params
|
211
225
|
)
|
212
226
|
logger.debug(f"Iteration {iterations}: Full Plan:", data=plan)
|
227
|
+
# Validate agent names in the plan early
|
228
|
+
self._validate_agent_names(plan)
|
213
229
|
else:
|
214
230
|
raise ValueError(f"Invalid plan type {self.plan_type}")
|
215
231
|
|
@@ -221,6 +237,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
221
237
|
plan_result.is_complete = True
|
222
238
|
|
223
239
|
# Synthesize final result into a single message
|
240
|
+
# Use the structured XML format for better context
|
224
241
|
synthesis_prompt = SYNTHESIZE_PLAN_PROMPT_TEMPLATE.format(
|
225
242
|
plan_result=format_plan_result(plan_result)
|
226
243
|
)
|
@@ -265,6 +282,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
265
282
|
"""Execute a step's subtasks in parallel and synthesize results"""
|
266
283
|
|
267
284
|
step_result = StepResult(step=step, task_results=[])
|
285
|
+
# Use structured XML format for context to help agents better understand the context
|
268
286
|
context = format_plan_result(previous_result)
|
269
287
|
|
270
288
|
# Execute tasks
|
@@ -272,16 +290,18 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
272
290
|
error_tasks = []
|
273
291
|
|
274
292
|
for task in step.tasks:
|
293
|
+
# Make sure we're using a valid agent name
|
275
294
|
agent = self.agents.get(task.agent)
|
276
295
|
if not agent:
|
277
|
-
#
|
296
|
+
# Log a more prominent error - this is a serious problem that shouldn't happen
|
297
|
+
# with the improved prompt
|
278
298
|
self.logger.error(
|
279
|
-
f"No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
|
299
|
+
f"AGENT VALIDATION ERROR: No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
|
280
300
|
)
|
281
301
|
error_tasks.append(
|
282
302
|
(
|
283
303
|
task,
|
284
|
-
f"Error: Agent '{task.agent}' not found. Available agents: {', '.join(self.agents.keys())}",
|
304
|
+
f"Error: Agent '{task.agent}' not found. This indicates a problem with the plan generation. Available agents: {', '.join(self.agents.keys())}",
|
285
305
|
)
|
286
306
|
)
|
287
307
|
continue
|
@@ -307,18 +327,29 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
307
327
|
|
308
328
|
if task_index < len(results):
|
309
329
|
result = results[task_index]
|
310
|
-
|
311
|
-
|
330
|
+
# Create a TaskWithResult that includes the agent name for attribution
|
331
|
+
task_model = task.model_dump()
|
332
|
+
task_result = TaskWithResult(
|
333
|
+
description=task_model["description"],
|
334
|
+
agent=task_model["agent"], # Track which agent produced this result
|
335
|
+
result=str(result)
|
312
336
|
)
|
337
|
+
step_result.add_task_result(task_result)
|
313
338
|
task_index += 1
|
314
339
|
|
315
340
|
# Add error task results
|
316
341
|
for task, error_message in error_tasks:
|
342
|
+
task_model = task.model_dump()
|
317
343
|
step_result.add_task_result(
|
318
|
-
TaskWithResult(
|
344
|
+
TaskWithResult(
|
345
|
+
description=task_model["description"],
|
346
|
+
agent=task_model["agent"],
|
347
|
+
result=f"ERROR: {error_message}"
|
348
|
+
)
|
319
349
|
)
|
320
350
|
|
321
|
-
|
351
|
+
# Use text formatting for display in logs
|
352
|
+
step_result.result = format_step_result_text(step_result)
|
322
353
|
return step_result
|
323
354
|
|
324
355
|
async def _get_full_plan(
|
@@ -328,30 +359,80 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
328
359
|
request_params: RequestParams | None = None,
|
329
360
|
) -> Plan:
|
330
361
|
"""Generate full plan considering previous results"""
|
362
|
+
import json
|
363
|
+
from pydantic import ValidationError
|
364
|
+
from mcp_agent.workflows.orchestrator.orchestrator_models import Plan, Step, AgentTask
|
365
|
+
|
331
366
|
params = self.get_request_params(request_params)
|
332
367
|
params = params.model_copy(update={"use_history": False})
|
333
368
|
|
369
|
+
# Format agents without numeric prefixes for cleaner XML
|
334
370
|
agents = "\n".join(
|
335
|
-
[
|
336
|
-
f"{idx}. {self._format_agent_info(agent)}"
|
337
|
-
for idx, agent in enumerate(self.agents, 1)
|
338
|
-
]
|
371
|
+
[self._format_agent_info(agent) for agent in self.agents]
|
339
372
|
)
|
340
373
|
|
374
|
+
# Create clear plan status indicator for the template
|
375
|
+
plan_status = "Plan Status: Not Started"
|
376
|
+
if hasattr(plan_result, "is_complete"):
|
377
|
+
plan_status = "Plan Status: Complete" if plan_result.is_complete else "Plan Status: In Progress"
|
378
|
+
|
341
379
|
prompt = FULL_PLAN_PROMPT_TEMPLATE.format(
|
342
380
|
objective=objective,
|
343
381
|
plan_result=format_plan_result(plan_result),
|
382
|
+
plan_status=plan_status,
|
344
383
|
agents=agents,
|
345
384
|
)
|
346
385
|
|
347
|
-
#
|
348
|
-
|
386
|
+
# Get raw JSON response from LLM
|
387
|
+
result_str = await self.planner.generate_str(
|
349
388
|
message=prompt,
|
350
|
-
response_model=Plan,
|
351
389
|
request_params=params,
|
352
390
|
)
|
353
|
-
|
354
|
-
|
391
|
+
|
392
|
+
try:
|
393
|
+
# Parse JSON directly
|
394
|
+
data = json.loads(result_str)
|
395
|
+
|
396
|
+
# Create models manually to ensure agent names are preserved exactly as returned
|
397
|
+
steps = []
|
398
|
+
for step_data in data.get('steps', []):
|
399
|
+
tasks = []
|
400
|
+
for task_data in step_data.get('tasks', []):
|
401
|
+
# Create AgentTask directly from dict, preserving exact agent string
|
402
|
+
task = AgentTask(
|
403
|
+
description=task_data.get('description', ''),
|
404
|
+
agent=task_data.get('agent', '') # Preserve exact agent name
|
405
|
+
)
|
406
|
+
tasks.append(task)
|
407
|
+
|
408
|
+
# Create Step with the exact task objects we created
|
409
|
+
step = Step(
|
410
|
+
description=step_data.get('description', ''),
|
411
|
+
tasks=tasks
|
412
|
+
)
|
413
|
+
steps.append(step)
|
414
|
+
|
415
|
+
# Create final Plan
|
416
|
+
plan = Plan(
|
417
|
+
steps=steps,
|
418
|
+
is_complete=data.get('is_complete', False)
|
419
|
+
)
|
420
|
+
|
421
|
+
return plan
|
422
|
+
|
423
|
+
except (json.JSONDecodeError, ValidationError, KeyError) as e:
|
424
|
+
# Log detailed error and fall back to the original method as last resort
|
425
|
+
self.logger.error(f"Error parsing plan JSON: {str(e)}")
|
426
|
+
self.logger.debug(f"Failed JSON content: {result_str}")
|
427
|
+
|
428
|
+
# Use the normal structured parsing as fallback
|
429
|
+
plan = await self.planner.generate_structured(
|
430
|
+
message=result_str,
|
431
|
+
response_model=Plan,
|
432
|
+
request_params=params,
|
433
|
+
)
|
434
|
+
|
435
|
+
return plan
|
355
436
|
|
356
437
|
async def _get_next_step(
|
357
438
|
self,
|
@@ -360,54 +441,127 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
|
|
360
441
|
request_params: RequestParams | None = None,
|
361
442
|
) -> NextStep:
|
362
443
|
"""Generate just the next needed step"""
|
444
|
+
import json
|
445
|
+
from pydantic import ValidationError
|
446
|
+
from mcp_agent.workflows.orchestrator.orchestrator_models import NextStep, AgentTask
|
447
|
+
|
363
448
|
params = self.get_request_params(request_params)
|
364
449
|
params = params.model_copy(update={"use_history": False})
|
365
450
|
|
451
|
+
# Format agents without numeric prefixes for cleaner XML
|
366
452
|
agents = "\n".join(
|
367
|
-
[
|
368
|
-
f"{idx}. {self._format_agent_info(agent)}"
|
369
|
-
for idx, agent in enumerate(self.agents, 1)
|
370
|
-
]
|
453
|
+
[self._format_agent_info(agent) for agent in self.agents]
|
371
454
|
)
|
372
455
|
|
456
|
+
# Create clear plan status indicator for the template
|
457
|
+
plan_status = "Plan Status: Not Started"
|
458
|
+
if hasattr(plan_result, "is_complete"):
|
459
|
+
plan_status = "Plan Status: Complete" if plan_result.is_complete else "Plan Status: In Progress"
|
460
|
+
|
373
461
|
prompt = ITERATIVE_PLAN_PROMPT_TEMPLATE.format(
|
374
462
|
objective=objective,
|
375
463
|
plan_result=format_plan_result(plan_result),
|
464
|
+
plan_status=plan_status,
|
376
465
|
agents=agents,
|
377
466
|
)
|
378
467
|
|
379
|
-
#
|
380
|
-
|
468
|
+
# Get raw JSON response from LLM
|
469
|
+
result_str = await self.planner.generate_str(
|
381
470
|
message=prompt,
|
382
|
-
response_model=NextStep,
|
383
471
|
request_params=params,
|
384
472
|
)
|
385
|
-
|
473
|
+
|
474
|
+
try:
|
475
|
+
# Parse JSON directly
|
476
|
+
data = json.loads(result_str)
|
477
|
+
|
478
|
+
# Create task objects manually to preserve exact agent names
|
479
|
+
tasks = []
|
480
|
+
for task_data in data.get('tasks', []):
|
481
|
+
# Preserve the exact agent name as specified in the JSON
|
482
|
+
task = AgentTask(
|
483
|
+
description=task_data.get('description', ''),
|
484
|
+
agent=task_data.get('agent', '')
|
485
|
+
)
|
486
|
+
tasks.append(task)
|
487
|
+
|
488
|
+
# Create step with manually constructed tasks
|
489
|
+
next_step = NextStep(
|
490
|
+
description=data.get('description', ''),
|
491
|
+
tasks=tasks,
|
492
|
+
is_complete=data.get('is_complete', False)
|
493
|
+
)
|
494
|
+
|
495
|
+
return next_step
|
496
|
+
|
497
|
+
except (json.JSONDecodeError, ValidationError, KeyError) as e:
|
498
|
+
# Log detailed error and fall back to the original method
|
499
|
+
self.logger.error(f"Error parsing next step JSON: {str(e)}")
|
500
|
+
self.logger.debug(f"Failed JSON content: {result_str}")
|
501
|
+
|
502
|
+
# Use the normal structured parsing as fallback
|
503
|
+
next_step = await self.planner.generate_structured(
|
504
|
+
message=result_str,
|
505
|
+
response_model=NextStep,
|
506
|
+
request_params=params,
|
507
|
+
)
|
508
|
+
|
509
|
+
return next_step
|
386
510
|
|
387
511
|
def _format_server_info(self, server_name: str) -> str:
|
388
|
-
"""Format server information for display to planners"""
|
512
|
+
"""Format server information for display to planners using XML tags"""
|
513
|
+
from mcp_agent.workflows.llm.prompt_utils import format_server_info
|
514
|
+
|
389
515
|
server_config = self.server_registry.get_server_config(server_name)
|
390
|
-
|
391
|
-
if not
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
516
|
+
|
517
|
+
# Get description or empty string if not available
|
518
|
+
description = ""
|
519
|
+
if server_config and server_config.description:
|
520
|
+
description = server_config.description
|
521
|
+
|
522
|
+
return format_server_info(server_name, description)
|
523
|
+
|
524
|
+
def _validate_agent_names(self, plan: Plan) -> None:
|
525
|
+
"""
|
526
|
+
Validate all agent names in a plan before execution.
|
527
|
+
This helps catch invalid agent references early.
|
528
|
+
"""
|
529
|
+
invalid_agents = []
|
530
|
+
|
531
|
+
for step in plan.steps:
|
532
|
+
for task in step.tasks:
|
533
|
+
if task.agent not in self.agents:
|
534
|
+
invalid_agents.append(task.agent)
|
535
|
+
|
536
|
+
if invalid_agents:
|
537
|
+
available_agents = ", ".join(self.agents.keys())
|
538
|
+
invalid_list = ", ".join(invalid_agents)
|
539
|
+
error_msg = f"Plan contains invalid agent names: {invalid_list}. Available agents: {available_agents}"
|
540
|
+
self.logger.error(error_msg)
|
541
|
+
# We don't raise an exception here as the execution will handle invalid agents
|
542
|
+
# by logging errors for individual tasks
|
543
|
+
|
400
544
|
def _format_agent_info(self, agent_name: str) -> str:
|
401
|
-
"""Format Agent information for display to planners"""
|
545
|
+
"""Format Agent information for display to planners using XML tags"""
|
546
|
+
from mcp_agent.workflows.llm.prompt_utils import format_agent_info
|
547
|
+
|
402
548
|
agent = self.agents.get(agent_name)
|
403
549
|
if not agent:
|
404
550
|
return ""
|
405
551
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
552
|
+
# Get agent instruction as string
|
553
|
+
instruction = agent.instruction
|
554
|
+
if callable(instruction):
|
555
|
+
instruction = instruction({})
|
556
|
+
|
557
|
+
# Get servers information
|
558
|
+
server_info = []
|
559
|
+
for server_name in agent.server_names:
|
560
|
+
server_config = self.server_registry.get_server_config(server_name)
|
561
|
+
description = ""
|
562
|
+
if server_config and server_config.description:
|
563
|
+
description = server_config.description
|
564
|
+
|
565
|
+
server_info.append({"name": server_name, "description": description})
|
566
|
+
|
567
|
+
return format_agent_info(agent.name, instruction, server_info if server_info else None)
|