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.
@@ -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
- # TODO: saqadri - would be useful to return the reason for stopping to the caller
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
- format_step_result,
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
- # Use AugmentedLLM's structured output handling
170
- return await super().generate_structured(
171
- message=result_str,
172
- response_model=response_model,
173
- request_params=params,
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
- # Instead of failing the entire step, track this as an error task
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
- step_result.add_task_result(
311
- TaskWithResult(**task.model_dump(), result=str(result))
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(**task.model_dump(), result=error_message)
344
+ TaskWithResult(
345
+ description=task_model["description"],
346
+ agent=task_model["agent"],
347
+ result=f"ERROR: {error_message}"
348
+ )
319
349
  )
320
350
 
321
- step_result.result = format_step_result(step_result)
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
- # Use planner directly - no verb manipulation needed
348
- plan = await self.planner.generate_structured(
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
- return plan
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
- # Use planner directly - no verb manipulation needed
380
- next_step = await self.planner.generate_structured(
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
- return next_step
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
- server_str = f"Server Name: {server_name}"
391
- if not server_config:
392
- return server_str
393
-
394
- description = server_config.description
395
- if description:
396
- server_str = f"{server_str}\nDescription: {description}"
397
-
398
- return server_str
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
- servers = "\n".join(
407
- [
408
- f"- {self._format_server_info(server_name)}"
409
- for server_name in agent.server_names
410
- ]
411
- )
412
-
413
- return f"Agent Name: {agent.name}\nDescription: {agent.instruction}\nServers in Agent: {servers}"
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)