fast-agent-mcp 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -0,0 +1,61 @@
1
+ from typing import Any, List, Optional, Type, Union
2
+
3
+ from pydantic_core import from_json
4
+ from mcp_agent.workflows.llm.augmented_llm import (
5
+ AugmentedLLM,
6
+ MessageParamT,
7
+ MessageT,
8
+ ModelT,
9
+ RequestParams,
10
+ )
11
+
12
+
13
+ class PassthroughLLM(AugmentedLLM):
14
+ """
15
+ A specialized LLM implementation that simply passes through input messages without modification.
16
+
17
+ This is useful for cases where you need an object with the AugmentedLLM interface
18
+ but want to preserve the original message without any processing, such as in a
19
+ parallel workflow where no fan-in aggregation is needed.
20
+ """
21
+
22
+ def __init__(self, name: str = "Passthrough", context=None, **kwargs):
23
+ super().__init__(name=name, context=context, **kwargs)
24
+
25
+ async def generate(
26
+ self,
27
+ message: Union[str, MessageParamT, List[MessageParamT]],
28
+ request_params: Optional[RequestParams] = None,
29
+ ) -> Union[List[MessageT], Any]:
30
+ """Simply return the input message as is."""
31
+ # Return in the format expected by the caller
32
+ return [message] if isinstance(message, list) else message
33
+
34
+ async def generate_str(
35
+ self,
36
+ message: Union[str, MessageParamT, List[MessageParamT]],
37
+ request_params: Optional[RequestParams] = None,
38
+ ) -> str:
39
+ """Return the input message as a string."""
40
+ self.show_user_message(message, model="fastagent-passthrough", chat_turn=0)
41
+ await self.show_assistant_message(message, title="ASSISTANT/PASSTHROUGH")
42
+
43
+ return str(message)
44
+
45
+ async def generate_structured(
46
+ self,
47
+ message: Union[str, MessageParamT, List[MessageParamT]],
48
+ response_model: Type[ModelT],
49
+ request_params: Optional[RequestParams] = None,
50
+ ) -> ModelT:
51
+ """
52
+ Return the input message as the requested model type.
53
+ This is a best-effort implementation - it may fail if the
54
+ message cannot be converted to the requested model.
55
+ """
56
+ if isinstance(message, response_model):
57
+ return message
58
+ elif isinstance(message, dict):
59
+ return response_model(**message)
60
+ elif isinstance(message, str):
61
+ return response_model.model_validate(from_json(message, allow_partial=True))
@@ -7,7 +7,7 @@ from mcp_agent.core.exceptions import ModelConfigError
7
7
  from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
8
8
  from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
9
9
  from mcp_agent.workflows.llm.augmented_llm import RequestParams
10
- from mcp_agent.workflows.llm.enhanced_passthrough import EnhancedPassthroughLLM
10
+ from mcp_agent.workflows.llm.augmented_llm_passthrough import PassthroughLLM
11
11
 
12
12
  # Type alias for LLM classes
13
13
  LLMClass = Union[Type[AnthropicAugmentedLLM], Type[OpenAIAugmentedLLM]]
@@ -18,7 +18,7 @@ class Provider(Enum):
18
18
 
19
19
  ANTHROPIC = auto()
20
20
  OPENAI = auto()
21
- SIMULATOR = auto()
21
+ FAST_AGENT = auto()
22
22
 
23
23
 
24
24
  class ReasoningEffort(Enum):
@@ -45,6 +45,7 @@ class ModelFactory:
45
45
  PROVIDER_MAP = {
46
46
  "anthropic": Provider.ANTHROPIC,
47
47
  "openai": Provider.OPENAI,
48
+ "fast-agent": Provider.FAST_AGENT,
48
49
  }
49
50
 
50
51
  # Mapping of effort strings to enum values
@@ -59,6 +60,7 @@ class ModelFactory:
59
60
  # TODO -- bring model parameter configuration here
60
61
  # Mapping of model names to their default providers
61
62
  DEFAULT_PROVIDERS = {
63
+ "passthrough": Provider.FAST_AGENT,
62
64
  "gpt-4o": Provider.OPENAI,
63
65
  "gpt-4o-mini": Provider.OPENAI,
64
66
  "o1-mini": Provider.OPENAI,
@@ -93,7 +95,7 @@ class ModelFactory:
93
95
  PROVIDER_CLASSES: Dict[Provider, LLMClass] = {
94
96
  Provider.ANTHROPIC: AnthropicAugmentedLLM,
95
97
  Provider.OPENAI: OpenAIAugmentedLLM,
96
- Provider.SIMULATOR: EnhancedPassthroughLLM,
98
+ Provider.FAST_AGENT: PassthroughLLM,
97
99
  }
98
100
 
99
101
  @classmethod
@@ -6,8 +6,8 @@ from typing import (
6
6
  List,
7
7
  Literal,
8
8
  Optional,
9
- Type,
10
9
  TYPE_CHECKING,
10
+ Type,
11
11
  )
12
12
 
13
13
  from mcp_agent.agents.agent import Agent
@@ -132,21 +132,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
132
132
  # Store agents by name - COMPLETE REWRITE OF AGENT STORAGE
133
133
  self.agents = {}
134
134
  for agent in available_agents:
135
- # Fix: Remove all special handling of agent names and store them exactly as they are
136
135
  agent_name = agent.name
137
-
138
- # Verify if the name is actually "None" (string) or None (NoneType)
139
- if agent_name == "None":
140
- # Try to get a better name from config if available
141
- if hasattr(agent, "config") and agent.config and agent.config.name:
142
- agent_name = agent.config.name
143
- elif agent_name is None:
144
- # Try to get a better name from config if available
145
- if hasattr(agent, "config") and agent.config and agent.config.name:
146
- agent_name = agent.config.name
147
- else:
148
- agent_name = f"unnamed_agent_{len(self.agents)}"
149
-
150
136
  self.logger.info(f"Adding agent '{agent_name}' to orchestrator")
151
137
  self.agents[agent_name] = agent
152
138
 
@@ -169,7 +155,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
169
155
  ) -> str:
170
156
  """Request an LLM generation and return the string representation of the result"""
171
157
  params = self.get_request_params(request_params)
172
- # TODO -- properly incorporate this in to message display etc.
158
+
173
159
  result = await self.generate(
174
160
  message=message,
175
161
  request_params=params,
@@ -183,30 +169,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
183
169
  response_model: Type[ModelT],
184
170
  request_params: RequestParams | None = None,
185
171
  ) -> ModelT:
186
- """Request a structured LLM generation and return the result as a Pydantic model."""
187
- import json
188
- from pydantic import ValidationError
189
-
190
- params = self.get_request_params(request_params)
191
- result_str = await self.generate_str(message=message, request_params=params)
192
-
193
- try:
194
- # Directly parse JSON and create model instance
195
- parsed_data = json.loads(result_str)
196
- return response_model(**parsed_data)
197
- except (json.JSONDecodeError, ValidationError) as e:
198
- # Log the error and fall back to the original method if direct parsing fails
199
- self.logger.error(
200
- f"Direct JSON parsing failed: {str(e)}. Falling back to standard method."
201
- )
202
- self.logger.debug(f"Failed JSON content: {result_str}")
203
-
204
- # Use AugmentedLLM's structured output handling as fallback
205
- return await super().generate_structured(
206
- message=result_str,
207
- response_model=response_model,
208
- request_params=params,
209
- )
172
+ return None
210
173
 
211
174
  async def execute(
212
175
  self, objective: str, request_params: RequestParams | None = None
@@ -299,9 +262,11 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
299
262
  plan_result.add_step_result(step_result)
300
263
  total_steps_executed += 1
301
264
 
302
- # Check for step limit after executing steps
303
- if total_steps_executed >= max_steps:
304
- plan_result.max_iterations_reached = True
265
+ # Check if we need to break from the main loop due to hitting max_steps
266
+ if (
267
+ hasattr(plan_result, "max_steps_reached")
268
+ and plan_result.max_steps_reached
269
+ ):
305
270
  break
306
271
 
307
272
  logger.debug(
@@ -317,21 +282,38 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
317
282
 
318
283
  iterations += 1
319
284
 
320
- # If we get here, we've hit the iteration limit without completing
321
- self.logger.warning(
322
- f"Failed to complete in {params.max_iterations} iterations."
323
- )
285
+ # If we reach here, either:
286
+ # 1. We hit iteration limit without completing
287
+ # 2. We hit max_steps limit without completing
288
+ # 3. We detected diminishing returns (plan with 0-1 steps after multiple iterations)
324
289
 
325
- # Mark that we hit the iteration limit
326
- plan_result.max_iterations_reached = True
290
+ # Check if we hit iteration limits without completing
291
+ if iterations >= params.max_iterations and not plan_result.is_complete:
292
+ self.logger.warning(
293
+ f"Failed to complete in {params.max_iterations} iterations."
294
+ )
295
+ # Mark that we hit the iteration limit
296
+ plan_result.max_iterations_reached = True
327
297
 
328
- # Synthesize what we have so far, but use a different prompt that explains the incomplete status
329
- synthesis_prompt = SYNTHESIZE_INCOMPLETE_PLAN_TEMPLATE.format(
330
- plan_result=format_plan_result(plan_result),
331
- max_iterations=params.max_iterations,
332
- )
298
+ # Use the incomplete template when we've hit iteration limits
299
+ synthesis_prompt = SYNTHESIZE_INCOMPLETE_PLAN_TEMPLATE.format(
300
+ plan_result=format_plan_result(plan_result),
301
+ max_iterations=params.max_iterations,
302
+ )
303
+ else:
304
+ # Either plan is complete or we had diminishing returns (which we mark as complete)
305
+ if not plan_result.is_complete:
306
+ self.logger.info(
307
+ "Plan terminated due to diminishing returns, marking as complete"
308
+ )
309
+ plan_result.is_complete = True
310
+
311
+ # Use standard template for complete plans
312
+ synthesis_prompt = SYNTHESIZE_PLAN_PROMPT_TEMPLATE.format(
313
+ plan_result=format_plan_result(plan_result)
314
+ )
333
315
 
334
- # Generate a final synthesis that acknowledges the incomplete status
316
+ # Generate the final synthesis with the appropriate template
335
317
  plan_result.result = await self.planner.generate_str(
336
318
  message=synthesis_prompt,
337
319
  request_params=params.model_copy(update={"max_iterations": 1}),
@@ -359,8 +341,6 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
359
341
  # Make sure we're using a valid agent name
360
342
  agent = self.agents.get(task.agent)
361
343
  if not agent:
362
- # Log a more prominent error - this is a serious problem that shouldn't happen
363
- # with the improved prompt
364
344
  self.logger.error(
365
345
  f"AGENT VALIDATION ERROR: No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
366
346
  )
@@ -431,18 +411,10 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
431
411
  request_params: RequestParams | None = None,
432
412
  ) -> Plan:
433
413
  """Generate full plan considering previous results"""
434
- import json
435
- from pydantic import ValidationError
436
- from mcp_agent.workflows.orchestrator.orchestrator_models import (
437
- Plan,
438
- Step,
439
- AgentTask,
440
- )
441
414
 
442
415
  params = self.get_request_params(request_params)
443
416
  params = params.model_copy(update={"use_history": False})
444
417
 
445
- # Format agents without numeric prefixes for cleaner XML
446
418
  agent_formats = []
447
419
  for agent_name in self.agents.keys():
448
420
  formatted = self._format_agent_info(agent_name)
@@ -452,7 +424,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
452
424
 
453
425
  # Create clear plan status indicator for the template
454
426
  plan_status = "Plan Status: Not Started"
455
- if hasattr(plan_result, "is_complete"):
427
+ if plan_result.is_complete:
456
428
  plan_status = (
457
429
  "Plan Status: Complete"
458
430
  if plan_result.is_complete
@@ -478,49 +450,30 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
478
450
  )
479
451
 
480
452
  # Get raw JSON response from LLM
481
- result_str = await self.planner.generate_str(
453
+ return await self.planner.generate_structured(
482
454
  message=prompt,
483
455
  request_params=params,
456
+ response_model=Plan,
484
457
  )
485
-
486
- try:
487
- # Parse JSON directly
488
- data = json.loads(result_str)
489
-
490
- # Create models manually to ensure agent names are preserved exactly as returned
491
- steps = []
492
- for step_data in data.get("steps", []):
493
- tasks = []
494
- for task_data in step_data.get("tasks", []):
495
- # Create AgentTask directly from dict, preserving exact agent string
496
- task = AgentTask(
497
- description=task_data.get("description", ""),
498
- agent=task_data.get("agent", ""), # Preserve exact agent name
499
- )
500
- tasks.append(task)
501
-
502
- # Create Step with the exact task objects we created
503
- step = Step(description=step_data.get("description", ""), tasks=tasks)
504
- steps.append(step)
505
-
506
- # Create final Plan
507
- plan = Plan(steps=steps, is_complete=data.get("is_complete", False))
508
-
509
- return plan
510
-
511
- except (json.JSONDecodeError, ValidationError, KeyError) as e:
512
- # Log detailed error and fall back to the original method as last resort
513
- self.logger.error(f"Error parsing plan JSON: {str(e)}")
514
- self.logger.debug(f"Failed JSON content: {result_str}")
515
-
516
- # Use the normal structured parsing as fallback
517
- plan = await self.planner.generate_structured(
518
- message=result_str,
519
- response_model=Plan,
520
- request_params=params,
521
- )
522
-
523
- return plan
458
+ # return data
459
+
460
+ # steps = []
461
+ # for step_data in data.steps:
462
+ # tasks = []
463
+ # for task_data in step_data.tasks:
464
+ # task = AgentTask(
465
+ # description=task_data.description,
466
+ # agent=task_data.agent,
467
+ # )
468
+ # tasks.append(task)
469
+
470
+ # # Create Step with the exact task objects we created
471
+ # step = Step(description=step_data.description, tasks=tasks)
472
+ # steps.append(step)
473
+
474
+ # # Create final Plan
475
+ # plan = Plan(steps=steps, is_complete=data.is_complete)
476
+ # return plan
524
477
 
525
478
  async def _get_next_step(
526
479
  self,
@@ -529,12 +482,6 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
529
482
  request_params: RequestParams | None = None,
530
483
  ) -> NextStep:
531
484
  """Generate just the next needed step"""
532
- import json
533
- from pydantic import ValidationError
534
- from mcp_agent.workflows.orchestrator.orchestrator_models import (
535
- NextStep,
536
- AgentTask,
537
- )
538
485
 
539
486
  params = self.get_request_params(request_params)
540
487
  params = params.model_copy(update={"use_history": False})
@@ -547,7 +494,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
547
494
 
548
495
  # Create clear plan status indicator for the template
549
496
  plan_status = "Plan Status: Not Started"
550
- if hasattr(plan_result, "is_complete"):
497
+ if plan_result:
551
498
  plan_status = (
552
499
  "Plan Status: Complete"
553
500
  if plan_result.is_complete
@@ -569,48 +516,10 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
569
516
  )
570
517
 
571
518
  # Get raw JSON response from LLM
572
- result_str = await self.planner.generate_str(
573
- message=prompt,
574
- request_params=params,
519
+ return await self.planner.generate_structured(
520
+ message=prompt, request_params=params, response_model=NextStep
575
521
  )
576
522
 
577
- try:
578
- # Parse JSON directly
579
- data = json.loads(result_str)
580
-
581
- # Create task objects manually to preserve exact agent names
582
- tasks = []
583
- for task_data in data.get("tasks", []):
584
- # Preserve the exact agent name as specified in the JSON
585
- task = AgentTask(
586
- description=task_data.get("description", ""),
587
- agent=task_data.get("agent", ""),
588
- )
589
- tasks.append(task)
590
-
591
- # Create step with manually constructed tasks
592
- next_step = NextStep(
593
- description=data.get("description", ""),
594
- tasks=tasks,
595
- is_complete=data.get("is_complete", False),
596
- )
597
-
598
- return next_step
599
-
600
- except (json.JSONDecodeError, ValidationError, KeyError) as e:
601
- # Log detailed error and fall back to the original method
602
- self.logger.error(f"Error parsing next step JSON: {str(e)}")
603
- self.logger.debug(f"Failed JSON content: {result_str}")
604
-
605
- # Use the normal structured parsing as fallback
606
- next_step = await self.planner.generate_structured(
607
- message=result_str,
608
- response_model=NextStep,
609
- request_params=params,
610
- )
611
-
612
- return next_step
613
-
614
523
  def _format_server_info(self, server_name: str) -> str:
615
524
  """Format server information for display to planners using XML tags"""
616
525
  from mcp_agent.workflows.llm.prompt_utils import format_server_info
@@ -13,7 +13,8 @@ if TYPE_CHECKING:
13
13
 
14
14
  logger = get_logger(__name__)
15
15
 
16
-
16
+ # TODO -- reinstate function/server routing
17
+ # TODO -- Generate the Example Schema from the Pydantic Model
17
18
  DEFAULT_ROUTING_INSTRUCTION = """
18
19
  You are a highly accurate request router that directs incoming requests to the most appropriate category.
19
20
  A category is a specialized destination, such as a Function, an MCP Server (a collection of tools/functions), or an Agent (a collection of servers).
@@ -34,7 +35,7 @@ Your task is to analyze the request and determine the most appropriate categorie
34
35
  - Whether the request might benefit from multiple categories (up to {top_k})
35
36
 
36
37
  <fastagent:instruction>
37
- Respond in JSON format:
38
+ Respond in JSON format. NEVER include Code Fences:
38
39
  {{
39
40
  "categories": [
40
41
  {{
@@ -65,37 +66,31 @@ Follow these guidelines:
65
66
  """
66
67
 
67
68
 
68
- class LLMRouterResult(RouterResult[ResultT]):
69
- """A class that represents the result of an LLMRouter.route request"""
69
+ class ConfidenceRating(BaseModel):
70
+ """Base class for models with confidence ratings and reasoning"""
70
71
 
71
- confidence: Literal["high", "medium", "low"]
72
72
  """The confidence level of the routing decision."""
73
+ confidence: Literal["high", "medium", "low"]
74
+ """A brief explanation of the routing decision."""
75
+ reasoning: str | None = None # Make nullable to support both use cases
73
76
 
74
- reasoning: str | None = None
75
- """
76
- A brief explanation of the routing decision.
77
- This is optional and may only be provided if the router is an LLM
78
- """
79
-
80
-
81
- class StructuredResponseCategory(BaseModel):
82
- """A class that represents a single category returned by an LLM router"""
83
77
 
84
- category: str
78
+ # Used for LLM output parsing
79
+ class StructuredResponseCategory(ConfidenceRating):
85
80
  """The name of the category (i.e. MCP server, Agent or function) to route the input to."""
86
81
 
87
- confidence: Literal["high", "medium", "low"]
88
- """The confidence level of the routing decision."""
89
-
90
- reasoning: str | None = None
91
- """A brief explanation of the routing decision."""
82
+ category: str # Category name for lookup
92
83
 
93
84
 
94
85
  class StructuredResponse(BaseModel):
95
- """A class that represents the structured response of an LLM router"""
96
-
97
86
  categories: List[StructuredResponseCategory]
98
- """A list of categories to route the input to."""
87
+
88
+
89
+ # Used for final router output
90
+ class LLMRouterResult(RouterResult[ResultT], ConfidenceRating):
91
+ # Inherits 'result' from RouterResult
92
+ # Inherits 'confidence' and 'reasoning' from ConfidenceRating
93
+ pass
99
94
 
100
95
 
101
96
  class LLMRouter(Router):
@@ -282,7 +277,6 @@ class LLMRouter(Router):
282
277
  for r in response.categories:
283
278
  router_category = self.categories.get(r.category)
284
279
  if not router_category:
285
- # Skip invalid categories
286
280
  # TODO: log or raise an error
287
281
  continue
288
282