fast-agent-mcp 0.0.13__py3-none-any.whl → 0.0.15__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.13.dist-info → fast_agent_mcp-0.0.15.dist-info}/METADATA +213 -15
- {fast_agent_mcp-0.0.13.dist-info → fast_agent_mcp-0.0.15.dist-info}/RECORD +20 -13
- mcp_agent/cli/commands/bootstrap.py +1 -1
- mcp_agent/core/agent_app.py +163 -0
- mcp_agent/core/agent_types.py +16 -0
- mcp_agent/core/agent_utils.py +65 -0
- mcp_agent/core/error_handling.py +23 -0
- mcp_agent/core/exceptions.py +7 -0
- mcp_agent/core/fastagent.py +407 -301
- mcp_agent/core/proxies.py +127 -0
- mcp_agent/core/types.py +22 -0
- mcp_agent/resources/examples/internal/social.py +66 -0
- mcp_agent/resources/examples/workflows/chaining.py +16 -6
- mcp_agent/resources/examples/workflows/parallel.py +4 -0
- mcp_agent/workflows/llm/augmented_llm.py +75 -1
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +49 -18
- mcp_agent/workflows/parallel/parallel_llm.py +32 -7
- {fast_agent_mcp-0.0.13.dist-info → fast_agent_mcp-0.0.15.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.0.13.dist-info → fast_agent_mcp-0.0.15.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.0.13.dist-info → fast_agent_mcp-0.0.15.dist-info}/licenses/LICENSE +0 -0
mcp_agent/core/fastagent.py
CHANGED
@@ -10,266 +10,57 @@ from typing import (
|
|
10
10
|
Callable,
|
11
11
|
TypeVar,
|
12
12
|
Any,
|
13
|
-
Union,
|
14
|
-
TypeAlias,
|
15
13
|
Literal,
|
16
14
|
)
|
17
|
-
from enum import Enum
|
18
15
|
import yaml
|
19
16
|
import argparse
|
20
17
|
from contextlib import asynccontextmanager
|
21
18
|
|
22
|
-
from mcp_agent.core.exceptions import (
|
23
|
-
AgentConfigError,
|
24
|
-
ModelConfigError,
|
25
|
-
PromptExitError,
|
26
|
-
ServerConfigError,
|
27
|
-
ProviderKeyError,
|
28
|
-
ServerInitializationError,
|
29
|
-
)
|
30
|
-
|
31
19
|
from mcp_agent.app import MCPApp
|
32
20
|
from mcp_agent.agents.agent import Agent, AgentConfig
|
33
21
|
from mcp_agent.context_dependent import ContextDependent
|
22
|
+
from mcp_agent.config import Settings
|
34
23
|
from mcp_agent.event_progress import ProgressAction
|
35
|
-
from mcp_agent.workflows.orchestrator.orchestrator import Orchestrator
|
36
|
-
from mcp_agent.workflows.parallel.parallel_llm import ParallelLLM
|
37
24
|
from mcp_agent.workflows.evaluator_optimizer.evaluator_optimizer import (
|
38
25
|
EvaluatorOptimizerLLM,
|
39
26
|
QualityRating,
|
40
27
|
)
|
41
|
-
from mcp_agent.workflows.router.router_llm import LLMRouter
|
42
|
-
from mcp_agent.config import Settings
|
43
|
-
from rich import print
|
44
|
-
from mcp_agent.progress_display import progress_display
|
45
|
-
from mcp_agent.workflows.llm.model_factory import ModelFactory
|
46
28
|
from mcp_agent.workflows.llm.augmented_llm import AugmentedLLM, RequestParams
|
29
|
+
from mcp_agent.workflows.llm.model_factory import ModelFactory
|
30
|
+
from mcp_agent.workflows.orchestrator.orchestrator import Orchestrator
|
31
|
+
from mcp_agent.workflows.parallel.parallel_llm import ParallelLLM
|
32
|
+
from mcp_agent.workflows.router.router_llm import LLMRouter
|
47
33
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
34
|
+
from mcp_agent.core.agent_app import AgentApp
|
35
|
+
from mcp_agent.core.agent_types import AgentType
|
36
|
+
from mcp_agent.core.agent_utils import unwrap_proxy, get_agent_instances, log_agent_load
|
37
|
+
from mcp_agent.core.error_handling import handle_error
|
38
|
+
from mcp_agent.core.proxies import (
|
39
|
+
BaseAgentProxy,
|
40
|
+
LLMAgentProxy,
|
41
|
+
WorkflowProxy,
|
42
|
+
RouterProxy,
|
43
|
+
ChainProxy,
|
44
|
+
)
|
45
|
+
from mcp_agent.core.types import AgentOrWorkflow, ProxyDict
|
46
|
+
from mcp_agent.core.exceptions import (
|
47
|
+
AgentConfigError,
|
48
|
+
CircularDependencyError,
|
49
|
+
ModelConfigError,
|
50
|
+
PromptExitError,
|
51
|
+
ServerConfigError,
|
52
|
+
ProviderKeyError,
|
53
|
+
ServerInitializationError,
|
54
|
+
)
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
PARALLEL = "parallel"
|
65
|
-
EVALUATOR_OPTIMIZER = "evaluator_optimizer"
|
66
|
-
ROUTER = "router"
|
56
|
+
# TODO -- reinstate once Windows&Python 3.13 platform issues are fixed
|
57
|
+
# import readline # noqa: F401
|
67
58
|
|
59
|
+
from rich import print
|
68
60
|
|
69
61
|
T = TypeVar("T") # For the wrapper classes
|
70
62
|
|
71
63
|
|
72
|
-
class BaseAgentProxy:
|
73
|
-
"""Base class for all proxy types"""
|
74
|
-
|
75
|
-
def __init__(self, app: MCPApp, name: str):
|
76
|
-
self._app = app
|
77
|
-
self._name = name
|
78
|
-
|
79
|
-
async def __call__(self, message: Optional[str] = None) -> str:
|
80
|
-
"""Allow: agent.researcher('message')"""
|
81
|
-
return await self.send(message)
|
82
|
-
|
83
|
-
async def send(self, message: Optional[str] = None) -> str:
|
84
|
-
"""Allow: agent.researcher.send('message')"""
|
85
|
-
if message is None:
|
86
|
-
return await self.prompt()
|
87
|
-
return await self.generate_str(message)
|
88
|
-
|
89
|
-
async def prompt(self, default_prompt: str = "") -> str:
|
90
|
-
"""Allow: agent.researcher.prompt()"""
|
91
|
-
return await self._app.prompt(self._name, default_prompt)
|
92
|
-
|
93
|
-
async def generate_str(self, message: str) -> str:
|
94
|
-
"""Generate response for a message - must be implemented by subclasses"""
|
95
|
-
raise NotImplementedError("Subclasses must implement generate_str")
|
96
|
-
|
97
|
-
|
98
|
-
class AgentProxy(BaseAgentProxy):
|
99
|
-
"""Legacy proxy for individual agent operations"""
|
100
|
-
|
101
|
-
async def generate_str(self, message: str) -> str:
|
102
|
-
return await self._app.send(self._name, message)
|
103
|
-
|
104
|
-
|
105
|
-
class LLMAgentProxy(BaseAgentProxy):
|
106
|
-
"""Proxy for regular agents that use _llm.generate_str()"""
|
107
|
-
|
108
|
-
def __init__(self, app: MCPApp, name: str, agent: Agent):
|
109
|
-
super().__init__(app, name)
|
110
|
-
self._agent = agent
|
111
|
-
|
112
|
-
async def generate_str(self, message: str) -> str:
|
113
|
-
return await self._agent._llm.generate_str(message)
|
114
|
-
|
115
|
-
|
116
|
-
class WorkflowProxy(BaseAgentProxy):
|
117
|
-
"""Proxy for workflow types that implement generate_str() directly"""
|
118
|
-
|
119
|
-
def __init__(self, app: MCPApp, name: str, workflow: WorkflowType):
|
120
|
-
super().__init__(app, name)
|
121
|
-
self._workflow = workflow
|
122
|
-
|
123
|
-
async def generate_str(self, message: str) -> str:
|
124
|
-
return await self._workflow.generate_str(message)
|
125
|
-
|
126
|
-
|
127
|
-
class RouterProxy(BaseAgentProxy):
|
128
|
-
"""Proxy for LLM Routers"""
|
129
|
-
|
130
|
-
def __init__(self, app: MCPApp, name: str, workflow: WorkflowType):
|
131
|
-
super().__init__(app, name)
|
132
|
-
self._workflow = workflow
|
133
|
-
|
134
|
-
async def generate_str(self, message: str) -> str:
|
135
|
-
results = await self._workflow.route(message)
|
136
|
-
if not results:
|
137
|
-
return "No appropriate route found for the request."
|
138
|
-
|
139
|
-
# Get the top result
|
140
|
-
top_result = results[0]
|
141
|
-
if isinstance(top_result.result, Agent):
|
142
|
-
# Agent route - delegate to the agent
|
143
|
-
agent = top_result.result
|
144
|
-
|
145
|
-
return await agent._llm.generate_str(message)
|
146
|
-
elif isinstance(top_result.result, str):
|
147
|
-
# Server route - use the router directly
|
148
|
-
return "Tool call requested by router - not yet supported"
|
149
|
-
|
150
|
-
return f"Routed to: {top_result.result} ({top_result.confidence}): {top_result.reasoning}"
|
151
|
-
|
152
|
-
|
153
|
-
class AgentApp:
|
154
|
-
"""Main application wrapper"""
|
155
|
-
|
156
|
-
def __init__(self, app: MCPApp, agents: ProxyDict):
|
157
|
-
self._app = app
|
158
|
-
self._agents = agents
|
159
|
-
# Optional: set default agent for direct calls
|
160
|
-
self._default = next(iter(agents)) if agents else None
|
161
|
-
|
162
|
-
async def send(self, agent_name: str, message: Optional[str]) -> str:
|
163
|
-
"""Core message sending"""
|
164
|
-
if agent_name not in self._agents:
|
165
|
-
raise ValueError(f"No agent named '{agent_name}'")
|
166
|
-
|
167
|
-
if not message or "" == message:
|
168
|
-
return await self.prompt(agent_name)
|
169
|
-
|
170
|
-
proxy = self._agents[agent_name]
|
171
|
-
return await proxy.generate_str(message)
|
172
|
-
|
173
|
-
async def prompt(self, agent_name: Optional[str] = None, default: str = "") -> str:
|
174
|
-
"""
|
175
|
-
Interactive prompt for sending messages with advanced features.
|
176
|
-
|
177
|
-
Args:
|
178
|
-
agent_name: Optional target agent name (uses default if not specified)
|
179
|
-
default: Default message to use when user presses enter
|
180
|
-
"""
|
181
|
-
from .enhanced_prompt import get_enhanced_input, handle_special_commands
|
182
|
-
|
183
|
-
agent = agent_name or self._default
|
184
|
-
|
185
|
-
if agent not in self._agents:
|
186
|
-
raise ValueError(f"No agent named '{agent}'")
|
187
|
-
|
188
|
-
# Pass all available agent names for auto-completion
|
189
|
-
available_agents = list(self._agents.keys())
|
190
|
-
|
191
|
-
# Create agent_types dictionary mapping agent names to their types
|
192
|
-
agent_types = {}
|
193
|
-
for name, proxy in self._agents.items():
|
194
|
-
# Determine agent type based on the proxy type
|
195
|
-
if isinstance(proxy, LLMAgentProxy):
|
196
|
-
# Convert AgentType.BASIC.value ("agent") to "Agent"
|
197
|
-
agent_types[name] = "Agent"
|
198
|
-
elif isinstance(proxy, RouterProxy):
|
199
|
-
agent_types[name] = "Router"
|
200
|
-
elif isinstance(proxy, WorkflowProxy):
|
201
|
-
# For workflow proxies, check the workflow type
|
202
|
-
workflow = proxy._workflow
|
203
|
-
if isinstance(workflow, Orchestrator):
|
204
|
-
agent_types[name] = "Orchestrator"
|
205
|
-
elif isinstance(workflow, ParallelLLM):
|
206
|
-
agent_types[name] = "Parallel"
|
207
|
-
elif isinstance(workflow, EvaluatorOptimizerLLM):
|
208
|
-
agent_types[name] = "Evaluator"
|
209
|
-
else:
|
210
|
-
agent_types[name] = "Workflow"
|
211
|
-
|
212
|
-
result = ""
|
213
|
-
while True:
|
214
|
-
with progress_display.paused():
|
215
|
-
# Use the enhanced input method with advanced features
|
216
|
-
user_input = await get_enhanced_input(
|
217
|
-
agent_name=agent,
|
218
|
-
default=default,
|
219
|
-
show_default=(default != ""),
|
220
|
-
show_stop_hint=True,
|
221
|
-
multiline=False, # Default to single-line mode
|
222
|
-
available_agent_names=available_agents,
|
223
|
-
syntax=None, # Can enable syntax highlighting for code input
|
224
|
-
agent_types=agent_types, # Pass agent types for display
|
225
|
-
)
|
226
|
-
|
227
|
-
# Handle special commands
|
228
|
-
command_result = await handle_special_commands(user_input, self)
|
229
|
-
|
230
|
-
# Check if we should switch agents
|
231
|
-
if (
|
232
|
-
isinstance(command_result, dict)
|
233
|
-
and "switch_agent" in command_result
|
234
|
-
):
|
235
|
-
agent = command_result["switch_agent"]
|
236
|
-
continue
|
237
|
-
|
238
|
-
# Skip further processing if command was handled
|
239
|
-
if command_result:
|
240
|
-
continue
|
241
|
-
|
242
|
-
if user_input.upper() == "STOP":
|
243
|
-
return
|
244
|
-
if user_input == "":
|
245
|
-
continue
|
246
|
-
|
247
|
-
result = await self.send(agent, user_input)
|
248
|
-
|
249
|
-
return result
|
250
|
-
|
251
|
-
def __getattr__(self, name: str) -> AgentProxy:
|
252
|
-
"""Support: agent.researcher"""
|
253
|
-
if name not in self._agents:
|
254
|
-
raise AttributeError(f"No agent named '{name}'")
|
255
|
-
return AgentProxy(self, name)
|
256
|
-
|
257
|
-
def __getitem__(self, name: str) -> AgentProxy:
|
258
|
-
"""Support: agent['researcher']"""
|
259
|
-
if name not in self._agents:
|
260
|
-
raise KeyError(f"No agent named '{name}'")
|
261
|
-
return AgentProxy(self, name)
|
262
|
-
|
263
|
-
async def __call__(
|
264
|
-
self, message: Optional[str] = "", agent_name: Optional[str] = None
|
265
|
-
) -> str:
|
266
|
-
"""Support: agent('message')"""
|
267
|
-
target = agent_name or self._default
|
268
|
-
if not target:
|
269
|
-
raise ValueError("No default agent available")
|
270
|
-
return await self.send(target, message)
|
271
|
-
|
272
|
-
|
273
64
|
class FastAgent(ContextDependent):
|
274
65
|
"""
|
275
66
|
A decorator-based interface for MCP Agent applications.
|
@@ -293,11 +84,27 @@ class FastAgent(ContextDependent):
|
|
293
84
|
"--model",
|
294
85
|
help="Override the default model for all agents. Precedence is default < config_file < command line < constructor",
|
295
86
|
)
|
87
|
+
parser.add_argument(
|
88
|
+
"--agent",
|
89
|
+
help="Specify the agent to send a message to (used with --message)",
|
90
|
+
)
|
91
|
+
parser.add_argument(
|
92
|
+
"-m", "--message",
|
93
|
+
help="Message to send to the specified agent (requires --agent)",
|
94
|
+
)
|
95
|
+
parser.add_argument(
|
96
|
+
"--quiet", action="store_true",
|
97
|
+
help="Disable progress display, tool and message logging for cleaner output",
|
98
|
+
)
|
296
99
|
self.args = parser.parse_args()
|
100
|
+
|
101
|
+
# Quiet mode will be handled in _load_config()
|
297
102
|
|
298
103
|
self.name = name
|
299
104
|
self.config_path = config_path
|
300
105
|
self._load_config()
|
106
|
+
|
107
|
+
# Create the MCPApp with the config
|
301
108
|
self.app = MCPApp(
|
302
109
|
name=name,
|
303
110
|
settings=Settings(**self.config) if hasattr(self, "config") else None,
|
@@ -323,8 +130,9 @@ class FastAgent(ContextDependent):
|
|
323
130
|
if agent_type not in [
|
324
131
|
AgentType.PARALLEL.value,
|
325
132
|
AgentType.EVALUATOR_OPTIMIZER.value,
|
133
|
+
AgentType.CHAIN.value,
|
326
134
|
]:
|
327
|
-
self.
|
135
|
+
log_agent_load(self.app, name)
|
328
136
|
if agent_type == AgentType.BASIC.value:
|
329
137
|
if not isinstance(instance, Agent):
|
330
138
|
raise TypeError(
|
@@ -355,6 +163,10 @@ class FastAgent(ContextDependent):
|
|
355
163
|
f"Expected LLMRouter instance for {name}, got {type(instance)}"
|
356
164
|
)
|
357
165
|
return RouterProxy(self.app, name, instance)
|
166
|
+
elif agent_type == AgentType.CHAIN.value:
|
167
|
+
# Chain proxy is directly returned from _create_agents_by_type
|
168
|
+
# No need for type checking as it's already a ChainProxy
|
169
|
+
return instance
|
358
170
|
else:
|
359
171
|
raise ValueError(f"Unknown agent type: {agent_type}")
|
360
172
|
|
@@ -367,7 +179,7 @@ class FastAgent(ContextDependent):
|
|
367
179
|
"""Load configuration from YAML file, properly handling without dotenv processing"""
|
368
180
|
if self.config_path:
|
369
181
|
with open(self.config_path) as f:
|
370
|
-
self.config = yaml.safe_load(f)
|
182
|
+
self.config = yaml.safe_load(f) or {}
|
371
183
|
|
372
184
|
def _validate_server_references(self) -> None:
|
373
185
|
"""
|
@@ -461,6 +273,15 @@ class FastAgent(ContextDependent):
|
|
461
273
|
f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
|
462
274
|
)
|
463
275
|
|
276
|
+
elif agent_type == AgentType.CHAIN.value:
|
277
|
+
# Check that all agents in the sequence exist
|
278
|
+
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
279
|
+
missing = [a for a in sequence if a not in available_components]
|
280
|
+
if missing:
|
281
|
+
raise AgentConfigError(
|
282
|
+
f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
|
283
|
+
)
|
284
|
+
|
464
285
|
def _get_model_factory(
|
465
286
|
self,
|
466
287
|
model: Optional[str] = None,
|
@@ -599,6 +420,7 @@ class FastAgent(ContextDependent):
|
|
599
420
|
def agent(
|
600
421
|
self,
|
601
422
|
name: str = "Agent",
|
423
|
+
instruction_or_kwarg: str = None,
|
602
424
|
*,
|
603
425
|
instruction: str = "You are a helpful agent.",
|
604
426
|
servers: List[str] = [],
|
@@ -612,13 +434,26 @@ class FastAgent(ContextDependent):
|
|
612
434
|
|
613
435
|
Args:
|
614
436
|
name: Name of the agent
|
615
|
-
|
437
|
+
instruction_or_kwarg: Optional positional parameter for instruction
|
438
|
+
instruction: Base instruction for the agent (keyword arg)
|
616
439
|
servers: List of server names the agent should connect to
|
617
440
|
model: Model specification string (highest precedence)
|
618
441
|
use_history: Whether to maintain conversation history
|
619
442
|
request_params: Additional request parameters for the LLM
|
620
443
|
human_input: Whether to enable human input capabilities
|
444
|
+
|
445
|
+
The instruction can be provided either as a second positional argument
|
446
|
+
or as a keyword argument. Positional argument takes precedence when both are provided.
|
447
|
+
|
448
|
+
Usage:
|
449
|
+
@fast.agent("agent_name", "Your instruction here") # Using positional arg
|
450
|
+
@fast.agent("agent_name", instruction="Your instruction here") # Using keyword arg
|
621
451
|
"""
|
452
|
+
# Use positional argument if provided, otherwise use keyword argument
|
453
|
+
final_instruction = (
|
454
|
+
instruction_or_kwarg if instruction_or_kwarg is not None else instruction
|
455
|
+
)
|
456
|
+
|
622
457
|
decorator = self._create_decorator(
|
623
458
|
AgentType.BASIC,
|
624
459
|
default_name="Agent",
|
@@ -626,7 +461,7 @@ class FastAgent(ContextDependent):
|
|
626
461
|
default_use_history=True,
|
627
462
|
)(
|
628
463
|
name=name,
|
629
|
-
instruction=
|
464
|
+
instruction=final_instruction,
|
630
465
|
servers=servers,
|
631
466
|
model=model,
|
632
467
|
use_history=use_history,
|
@@ -687,25 +522,47 @@ class FastAgent(ContextDependent):
|
|
687
522
|
def parallel(
|
688
523
|
self,
|
689
524
|
name: str,
|
690
|
-
fan_in: str,
|
691
525
|
fan_out: List[str],
|
526
|
+
fan_in: Optional[str] = None,
|
692
527
|
instruction: str = "",
|
693
528
|
model: str | None = None,
|
694
529
|
use_history: bool = True,
|
695
530
|
request_params: Optional[Dict] = None,
|
531
|
+
include_request: bool = True,
|
696
532
|
) -> Callable:
|
697
533
|
"""
|
698
534
|
Decorator to create and register a parallel executing agent.
|
699
535
|
|
700
536
|
Args:
|
701
537
|
name: Name of the parallel executing agent
|
702
|
-
fan_in: Name of collecting agent
|
703
538
|
fan_out: List of parallel execution agents
|
539
|
+
fan_in: Optional name of collecting agent. If not provided, a passthrough agent
|
540
|
+
will be created automatically with the name "{name}_fan_in"
|
704
541
|
instruction: Optional instruction for the parallel agent
|
705
542
|
model: Model specification string
|
706
543
|
use_history: Whether to maintain conversation history
|
707
544
|
request_params: Additional request parameters for the LLM
|
545
|
+
include_request: Whether to include the original request in the fan-in message
|
708
546
|
"""
|
547
|
+
# If fan_in is not provided, create a passthrough agent with a derived name
|
548
|
+
if fan_in is None:
|
549
|
+
passthrough_name = f"{name}_fan_in"
|
550
|
+
|
551
|
+
# Register the passthrough agent directly in self.agents
|
552
|
+
self.agents[passthrough_name] = {
|
553
|
+
"config": AgentConfig(
|
554
|
+
name=passthrough_name,
|
555
|
+
instruction=f"Passthrough fan-in for {name}",
|
556
|
+
servers=[],
|
557
|
+
use_history=use_history,
|
558
|
+
),
|
559
|
+
"type": AgentType.BASIC.value, # Using BASIC type since we're just attaching a PassthroughLLM
|
560
|
+
"func": lambda x: x, # Simple passthrough function (never actually called)
|
561
|
+
}
|
562
|
+
|
563
|
+
# Use this passthrough as the fan-in
|
564
|
+
fan_in = passthrough_name
|
565
|
+
|
709
566
|
decorator = self._create_decorator(
|
710
567
|
AgentType.PARALLEL,
|
711
568
|
default_instruction="",
|
@@ -719,6 +576,7 @@ class FastAgent(ContextDependent):
|
|
719
576
|
model=model,
|
720
577
|
use_history=use_history,
|
721
578
|
request_params=request_params,
|
579
|
+
include_request=include_request,
|
722
580
|
)
|
723
581
|
return decorator
|
724
582
|
|
@@ -799,6 +657,88 @@ class FastAgent(ContextDependent):
|
|
799
657
|
)
|
800
658
|
return decorator
|
801
659
|
|
660
|
+
def chain(
|
661
|
+
self,
|
662
|
+
name: str = "Chain",
|
663
|
+
*,
|
664
|
+
sequence: List[str] = None,
|
665
|
+
agents: List[str] = None, # Alias for sequence
|
666
|
+
instruction: str = None,
|
667
|
+
model: str | None = None,
|
668
|
+
use_history: bool = True,
|
669
|
+
request_params: Optional[Dict] = None,
|
670
|
+
continue_with_final: bool = True,
|
671
|
+
) -> Callable:
|
672
|
+
"""
|
673
|
+
Decorator to create and register a chain of agents.
|
674
|
+
|
675
|
+
Args:
|
676
|
+
name: Name of the chain
|
677
|
+
sequence: List of agent names in order of execution (preferred name)
|
678
|
+
agents: Alias for sequence (backwards compatibility)
|
679
|
+
instruction: Optional custom instruction for the chain (if none provided, will autogenerate based on sequence)
|
680
|
+
model: Model specification string (not used directly in chain)
|
681
|
+
use_history: Whether to maintain conversation history
|
682
|
+
request_params: Additional request parameters
|
683
|
+
continue_with_final: When using prompt(), whether to continue with the final agent after processing chain (default: True)
|
684
|
+
"""
|
685
|
+
# Support both parameter names
|
686
|
+
agent_sequence = sequence or agents
|
687
|
+
if agent_sequence is None:
|
688
|
+
raise ValueError("Either 'sequence' or 'agents' parameter must be provided")
|
689
|
+
|
690
|
+
# Auto-generate instruction if not provided
|
691
|
+
if instruction is None:
|
692
|
+
# We'll generate it later when we have access to the agent configs and can see servers
|
693
|
+
instruction = f"Chain of agents: {', '.join(agent_sequence)}"
|
694
|
+
|
695
|
+
decorator = self._create_decorator(
|
696
|
+
AgentType.CHAIN,
|
697
|
+
default_name="Chain",
|
698
|
+
default_instruction=instruction,
|
699
|
+
default_use_history=True,
|
700
|
+
wrapper_needed=True,
|
701
|
+
)(
|
702
|
+
name=name,
|
703
|
+
sequence=agent_sequence,
|
704
|
+
instruction=instruction,
|
705
|
+
model=model,
|
706
|
+
use_history=use_history,
|
707
|
+
request_params=request_params,
|
708
|
+
continue_with_final=continue_with_final,
|
709
|
+
)
|
710
|
+
return decorator
|
711
|
+
|
712
|
+
def passthrough(
|
713
|
+
self,
|
714
|
+
name: str = "Passthrough",
|
715
|
+
use_history: bool = True,
|
716
|
+
**kwargs
|
717
|
+
) -> Callable:
|
718
|
+
"""
|
719
|
+
Decorator to create and register a passthrough agent.
|
720
|
+
A passthrough agent simply returns any input message without modification.
|
721
|
+
|
722
|
+
This is useful for parallel workflows where no fan-in aggregation is needed
|
723
|
+
(the fan-in agent can be a passthrough that simply returns the combined outputs).
|
724
|
+
|
725
|
+
Args:
|
726
|
+
name: Name of the passthrough agent
|
727
|
+
use_history: Whether to maintain conversation history
|
728
|
+
**kwargs: Additional parameters (ignored, for compatibility)
|
729
|
+
"""
|
730
|
+
decorator = self._create_decorator(
|
731
|
+
AgentType.BASIC, # Using BASIC agent type since we'll use a regular agent with PassthroughLLM
|
732
|
+
default_name="Passthrough",
|
733
|
+
default_instruction="Passthrough agent that returns input without modification",
|
734
|
+
default_use_history=use_history,
|
735
|
+
wrapper_needed=True,
|
736
|
+
)(
|
737
|
+
name=name,
|
738
|
+
use_history=use_history,
|
739
|
+
)
|
740
|
+
return decorator
|
741
|
+
|
802
742
|
async def _create_agents_by_type(
|
803
743
|
self,
|
804
744
|
agent_app: MCPApp,
|
@@ -832,19 +772,42 @@ class FastAgent(ContextDependent):
|
|
832
772
|
|
833
773
|
# Type-specific initialization
|
834
774
|
if agent_type == AgentType.BASIC:
|
835
|
-
#
|
836
|
-
|
837
|
-
|
838
|
-
#
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
agent
|
845
|
-
|
846
|
-
|
847
|
-
|
775
|
+
# Get the agent name for special handling
|
776
|
+
agent_name = agent_data["config"].name
|
777
|
+
|
778
|
+
# Check if this is an agent that should use the PassthroughLLM
|
779
|
+
if agent_name.endswith("_fan_in") or agent_name.startswith("passthrough"):
|
780
|
+
# Import here to avoid circular imports
|
781
|
+
from mcp_agent.workflows.llm.augmented_llm import PassthroughLLM
|
782
|
+
|
783
|
+
# Create basic agent with configuration
|
784
|
+
agent = Agent(config=config, context=agent_app.context)
|
785
|
+
|
786
|
+
# Set up a PassthroughLLM directly
|
787
|
+
async with agent:
|
788
|
+
agent._llm = PassthroughLLM(
|
789
|
+
name=f"{config.name}_llm",
|
790
|
+
context=agent_app.context,
|
791
|
+
agent=agent,
|
792
|
+
default_request_params=config.default_request_params,
|
793
|
+
)
|
794
|
+
|
795
|
+
# Store the agent
|
796
|
+
instance = agent
|
797
|
+
else:
|
798
|
+
# Standard basic agent with LLM
|
799
|
+
agent = Agent(config=config, context=agent_app.context)
|
800
|
+
|
801
|
+
# Set up LLM with proper configuration
|
802
|
+
async with agent:
|
803
|
+
llm_factory = self._get_model_factory(
|
804
|
+
model=config.model,
|
805
|
+
request_params=config.default_request_params,
|
806
|
+
)
|
807
|
+
agent._llm = await agent.attach_llm(llm_factory)
|
808
|
+
|
809
|
+
# Store the agent
|
810
|
+
instance = agent
|
848
811
|
|
849
812
|
elif agent_type == AgentType.ORCHESTRATOR:
|
850
813
|
# Get base params configured with model settings
|
@@ -958,6 +921,54 @@ class FastAgent(ContextDependent):
|
|
958
921
|
verb=ProgressAction.ROUTING, # Set verb for progress display
|
959
922
|
)
|
960
923
|
|
924
|
+
elif agent_type == AgentType.CHAIN:
|
925
|
+
# Get the sequence from either parameter
|
926
|
+
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
927
|
+
|
928
|
+
# Auto-generate instruction if not provided or if it's just the default
|
929
|
+
default_instruction = f"Chain of agents: {', '.join(sequence)}"
|
930
|
+
|
931
|
+
# If user provided a custom instruction, use that
|
932
|
+
# Otherwise, generate a description based on the sequence and their servers
|
933
|
+
if config.instruction == default_instruction:
|
934
|
+
# Get all agent names in the sequence
|
935
|
+
agent_names = []
|
936
|
+
all_servers = set()
|
937
|
+
|
938
|
+
# Collect information about the agents and their servers
|
939
|
+
for agent_name in sequence:
|
940
|
+
if agent_name in active_agents:
|
941
|
+
agent_proxy = active_agents[agent_name]
|
942
|
+
if hasattr(agent_proxy, "_agent"):
|
943
|
+
# For LLMAgentProxy
|
944
|
+
agent_instance = agent_proxy._agent
|
945
|
+
agent_names.append(agent_name)
|
946
|
+
if hasattr(agent_instance, "server_names"):
|
947
|
+
all_servers.update(agent_instance.server_names)
|
948
|
+
elif hasattr(agent_proxy, "_workflow"):
|
949
|
+
# For WorkflowProxy
|
950
|
+
agent_names.append(agent_name)
|
951
|
+
|
952
|
+
# Generate a better description
|
953
|
+
if agent_names:
|
954
|
+
server_part = (
|
955
|
+
f" with access to servers: {', '.join(sorted(all_servers))}"
|
956
|
+
if all_servers
|
957
|
+
else ""
|
958
|
+
)
|
959
|
+
config.instruction = f"Sequence of agents: {', '.join(agent_names)}{server_part}."
|
960
|
+
|
961
|
+
# Create a ChainProxy without needing a new instance
|
962
|
+
# Just pass the agent proxies and sequence
|
963
|
+
instance = ChainProxy(self.app, name, sequence, active_agents)
|
964
|
+
# Set continue_with_final behavior from configuration
|
965
|
+
instance._continue_with_final = agent_data.get(
|
966
|
+
"continue_with_final", True
|
967
|
+
)
|
968
|
+
|
969
|
+
# We removed the AgentType.PASSTHROUGH case
|
970
|
+
# Passthrough agents are now created as BASIC agents with a special LLM
|
971
|
+
|
961
972
|
elif agent_type == AgentType.PARALLEL:
|
962
973
|
# Get fan-out agents (could be basic agents or other parallels)
|
963
974
|
fan_out_agents = self._get_agent_instances(
|
@@ -979,6 +990,7 @@ class FastAgent(ContextDependent):
|
|
979
990
|
context=agent_app.context,
|
980
991
|
llm_factory=llm_factory,
|
981
992
|
default_request_params=config.default_request_params,
|
993
|
+
include_request=agent_data.get("include_request", True),
|
982
994
|
)
|
983
995
|
|
984
996
|
else:
|
@@ -1037,16 +1049,18 @@ class FastAgent(ContextDependent):
|
|
1037
1049
|
agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
|
1038
1050
|
)
|
1039
1051
|
|
1040
|
-
def
|
1041
|
-
self, name: str, visited: set, path: set
|
1052
|
+
def _get_dependencies(
|
1053
|
+
self, name: str, visited: set, path: set, agent_type: AgentType = None
|
1042
1054
|
) -> List[str]:
|
1043
1055
|
"""
|
1044
|
-
Get dependencies for
|
1056
|
+
Get dependencies for an agent in topological order.
|
1057
|
+
Works for both Parallel and Chain workflows.
|
1045
1058
|
|
1046
1059
|
Args:
|
1047
|
-
name: Name of the
|
1060
|
+
name: Name of the agent
|
1048
1061
|
visited: Set of already visited agents
|
1049
1062
|
path: Current path for cycle detection
|
1063
|
+
agent_type: Optional type filter (e.g., only check Parallel or Chain)
|
1050
1064
|
|
1051
1065
|
Returns:
|
1052
1066
|
List of agent names in dependency order
|
@@ -1056,7 +1070,7 @@ class FastAgent(ContextDependent):
|
|
1056
1070
|
"""
|
1057
1071
|
if name in path:
|
1058
1072
|
path_str = " -> ".join(path)
|
1059
|
-
raise
|
1073
|
+
raise CircularDependencyError(f"Path: {path_str} -> {name}")
|
1060
1074
|
|
1061
1075
|
if name in visited:
|
1062
1076
|
return []
|
@@ -1065,15 +1079,26 @@ class FastAgent(ContextDependent):
|
|
1065
1079
|
return []
|
1066
1080
|
|
1067
1081
|
config = self.agents[name]
|
1068
|
-
|
1082
|
+
|
1083
|
+
# Skip if not the requested type (when filtering)
|
1084
|
+
if agent_type and config["type"] != agent_type.value:
|
1069
1085
|
return []
|
1070
1086
|
|
1071
1087
|
path.add(name)
|
1072
1088
|
deps = []
|
1073
1089
|
|
1074
|
-
#
|
1075
|
-
|
1076
|
-
|
1090
|
+
# Handle dependencies based on agent type
|
1091
|
+
if config["type"] == AgentType.PARALLEL.value:
|
1092
|
+
# Get dependencies from fan-out agents
|
1093
|
+
for fan_out in config["fan_out"]:
|
1094
|
+
deps.extend(self._get_dependencies(fan_out, visited, path, agent_type))
|
1095
|
+
elif config["type"] == AgentType.CHAIN.value:
|
1096
|
+
# Get dependencies from sequence agents
|
1097
|
+
sequence = config.get("sequence", config.get("agents", []))
|
1098
|
+
for agent_name in sequence:
|
1099
|
+
deps.extend(
|
1100
|
+
self._get_dependencies(agent_name, visited, path, agent_type)
|
1101
|
+
)
|
1077
1102
|
|
1078
1103
|
# Add this agent after its dependencies
|
1079
1104
|
deps.append(name)
|
@@ -1082,54 +1107,95 @@ class FastAgent(ContextDependent):
|
|
1082
1107
|
|
1083
1108
|
return deps
|
1084
1109
|
|
1085
|
-
|
1086
|
-
self,
|
1110
|
+
def _get_parallel_dependencies(
|
1111
|
+
self, name: str, visited: set, path: set
|
1112
|
+
) -> List[str]:
|
1113
|
+
"""
|
1114
|
+
Get dependencies for a parallel agent in topological order.
|
1115
|
+
Legacy function that calls the more general _get_dependencies.
|
1116
|
+
|
1117
|
+
Args:
|
1118
|
+
name: Name of the parallel agent
|
1119
|
+
visited: Set of already visited agents
|
1120
|
+
path: Current path for cycle detection
|
1121
|
+
|
1122
|
+
Returns:
|
1123
|
+
List of agent names in dependency order
|
1124
|
+
|
1125
|
+
Raises:
|
1126
|
+
ValueError: If circular dependency detected
|
1127
|
+
"""
|
1128
|
+
return self._get_dependencies(name, visited, path, AgentType.PARALLEL)
|
1129
|
+
|
1130
|
+
async def _create_agents_in_dependency_order(
|
1131
|
+
self, agent_app: MCPApp, active_agents: ProxyDict, agent_type: AgentType
|
1087
1132
|
) -> ProxyDict:
|
1088
1133
|
"""
|
1089
|
-
Create
|
1134
|
+
Create agents in dependency order to avoid circular references.
|
1135
|
+
Works for both Parallel and Chain workflows.
|
1090
1136
|
|
1091
1137
|
Args:
|
1092
1138
|
agent_app: The main application instance
|
1093
1139
|
active_agents: Dictionary of already created agents/proxies
|
1140
|
+
agent_type: Type of agents to create (AgentType.PARALLEL or AgentType.CHAIN)
|
1094
1141
|
|
1095
1142
|
Returns:
|
1096
|
-
Dictionary of initialized
|
1143
|
+
Dictionary of initialized agents
|
1097
1144
|
"""
|
1098
|
-
|
1145
|
+
result_agents = {}
|
1099
1146
|
visited = set()
|
1100
1147
|
|
1101
|
-
# Get all
|
1102
|
-
|
1148
|
+
# Get all agents of the specified type
|
1149
|
+
agent_names = [
|
1103
1150
|
name
|
1104
1151
|
for name, agent_data in self.agents.items()
|
1105
|
-
if agent_data["type"] ==
|
1152
|
+
if agent_data["type"] == agent_type.value
|
1106
1153
|
]
|
1107
1154
|
|
1108
1155
|
# Create agents in dependency order
|
1109
|
-
for name in
|
1156
|
+
for name in agent_names:
|
1110
1157
|
# Get ordered dependencies if not already processed
|
1111
1158
|
if name not in visited:
|
1112
1159
|
try:
|
1113
|
-
ordered_agents = self.
|
1114
|
-
name, visited, set()
|
1160
|
+
ordered_agents = self._get_dependencies(
|
1161
|
+
name, visited, set(), agent_type
|
1115
1162
|
)
|
1116
1163
|
except ValueError as e:
|
1117
|
-
raise ValueError(
|
1164
|
+
raise ValueError(
|
1165
|
+
f"Error creating {agent_type.name.lower()} agent {name}: {str(e)}"
|
1166
|
+
)
|
1118
1167
|
|
1119
1168
|
# Create each agent in order
|
1120
1169
|
for agent_name in ordered_agents:
|
1121
|
-
if agent_name not in
|
1122
|
-
# Create one
|
1170
|
+
if agent_name not in result_agents:
|
1171
|
+
# Create one agent at a time using the generic method
|
1123
1172
|
agent_result = await self._create_agents_by_type(
|
1124
1173
|
agent_app,
|
1125
|
-
|
1174
|
+
agent_type,
|
1126
1175
|
active_agents,
|
1127
1176
|
agent_name=agent_name,
|
1128
1177
|
)
|
1129
1178
|
if agent_name in agent_result:
|
1130
|
-
|
1179
|
+
result_agents[agent_name] = agent_result[agent_name]
|
1131
1180
|
|
1132
|
-
return
|
1181
|
+
return result_agents
|
1182
|
+
|
1183
|
+
async def _create_parallel_agents(
|
1184
|
+
self, agent_app: MCPApp, active_agents: ProxyDict
|
1185
|
+
) -> ProxyDict:
|
1186
|
+
"""
|
1187
|
+
Create parallel execution agents in dependency order.
|
1188
|
+
|
1189
|
+
Args:
|
1190
|
+
agent_app: The main application instance
|
1191
|
+
active_agents: Dictionary of already created agents/proxies
|
1192
|
+
|
1193
|
+
Returns:
|
1194
|
+
Dictionary of initialized parallel agents
|
1195
|
+
"""
|
1196
|
+
return await self._create_agents_in_dependency_order(
|
1197
|
+
agent_app, active_agents, AgentType.PARALLEL
|
1198
|
+
)
|
1133
1199
|
|
1134
1200
|
async def _create_routers(
|
1135
1201
|
self, agent_app: MCPApp, active_agents: ProxyDict
|
@@ -1158,9 +1224,7 @@ class FastAgent(ContextDependent):
|
|
1158
1224
|
Returns:
|
1159
1225
|
The underlying Agent or workflow instance
|
1160
1226
|
"""
|
1161
|
-
|
1162
|
-
return proxy._agent
|
1163
|
-
return proxy._workflow
|
1227
|
+
return unwrap_proxy(proxy)
|
1164
1228
|
|
1165
1229
|
def _get_agent_instances(
|
1166
1230
|
self, agent_names: List[str], active_agents: ProxyDict
|
@@ -1175,7 +1239,7 @@ class FastAgent(ContextDependent):
|
|
1175
1239
|
Returns:
|
1176
1240
|
List of unwrapped agent/workflow instances
|
1177
1241
|
"""
|
1178
|
-
return
|
1242
|
+
return get_agent_instances(agent_names, active_agents)
|
1179
1243
|
|
1180
1244
|
@asynccontextmanager
|
1181
1245
|
async def run(self):
|
@@ -1185,14 +1249,30 @@ class FastAgent(ContextDependent):
|
|
1185
1249
|
"""
|
1186
1250
|
active_agents = {}
|
1187
1251
|
had_error = False
|
1252
|
+
|
1253
|
+
# Handle quiet mode by disabling logger settings after initialization
|
1254
|
+
quiet_mode = hasattr(self, "args") and self.args.quiet
|
1255
|
+
|
1188
1256
|
try:
|
1189
1257
|
async with self.app.run() as agent_app:
|
1258
|
+
# Apply quiet mode directly to the context's config if needed
|
1259
|
+
if quiet_mode and hasattr(agent_app.context, "config") and hasattr(agent_app.context.config, "logger"):
|
1260
|
+
# Apply after initialization but before agents are created
|
1261
|
+
agent_app.context.config.logger.progress_display = False
|
1262
|
+
agent_app.context.config.logger.show_chat = False
|
1263
|
+
agent_app.context.config.logger.show_tools = False
|
1264
|
+
|
1265
|
+
# Directly disable the progress display singleton
|
1266
|
+
from mcp_agent.progress_display import progress_display
|
1267
|
+
progress_display.stop() # This will stop and hide the display
|
1268
|
+
|
1190
1269
|
# Pre-flight validation
|
1191
1270
|
self._validate_server_references()
|
1192
1271
|
self._validate_workflow_references()
|
1193
1272
|
|
1194
1273
|
# Create all types of agents in dependency order
|
1195
1274
|
active_agents = await self._create_basic_agents(agent_app)
|
1275
|
+
|
1196
1276
|
orchestrators = await self._create_orchestrators(
|
1197
1277
|
agent_app, active_agents
|
1198
1278
|
)
|
@@ -1203,15 +1283,43 @@ class FastAgent(ContextDependent):
|
|
1203
1283
|
agent_app, active_agents
|
1204
1284
|
)
|
1205
1285
|
routers = await self._create_routers(agent_app, active_agents)
|
1286
|
+
chains = await self._create_agents_in_dependency_order(
|
1287
|
+
agent_app, active_agents, AgentType.CHAIN
|
1288
|
+
)
|
1206
1289
|
|
1207
1290
|
# Merge all agents into active_agents
|
1208
1291
|
active_agents.update(orchestrators)
|
1209
1292
|
active_agents.update(parallel_agents)
|
1210
1293
|
active_agents.update(evaluator_optimizers)
|
1211
1294
|
active_agents.update(routers)
|
1295
|
+
active_agents.update(chains)
|
1212
1296
|
|
1213
1297
|
# Create wrapper with all agents
|
1214
1298
|
wrapper = AgentApp(agent_app, active_agents)
|
1299
|
+
|
1300
|
+
# Handle direct message sending if --agent and --message are provided
|
1301
|
+
if self.args.agent and self.args.message:
|
1302
|
+
agent_name = self.args.agent
|
1303
|
+
message = self.args.message
|
1304
|
+
|
1305
|
+
if agent_name not in active_agents:
|
1306
|
+
available_agents = ", ".join(active_agents.keys())
|
1307
|
+
print(f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}")
|
1308
|
+
raise SystemExit(1)
|
1309
|
+
|
1310
|
+
try:
|
1311
|
+
# Get response
|
1312
|
+
response = await wrapper[agent_name].send(message)
|
1313
|
+
|
1314
|
+
# Only print the response in quiet mode
|
1315
|
+
if self.args.quiet:
|
1316
|
+
print(f"{response}")
|
1317
|
+
|
1318
|
+
raise SystemExit(0)
|
1319
|
+
except Exception as e:
|
1320
|
+
print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
|
1321
|
+
raise SystemExit(1)
|
1322
|
+
|
1215
1323
|
yield wrapper
|
1216
1324
|
|
1217
1325
|
except ServerConfigError as e:
|
@@ -1259,6 +1367,15 @@ class FastAgent(ContextDependent):
|
|
1259
1367
|
)
|
1260
1368
|
raise SystemExit(1)
|
1261
1369
|
|
1370
|
+
except CircularDependencyError as e:
|
1371
|
+
had_error = True
|
1372
|
+
self._handle_error(
|
1373
|
+
e,
|
1374
|
+
"Circular Dependency Detected",
|
1375
|
+
"Check your agent configuration for circular dependencies.",
|
1376
|
+
)
|
1377
|
+
raise SystemExit(1)
|
1378
|
+
|
1262
1379
|
except PromptExitError as e:
|
1263
1380
|
had_error = True
|
1264
1381
|
self._handle_error(
|
@@ -1289,19 +1406,8 @@ class FastAgent(ContextDependent):
|
|
1289
1406
|
error_type: Type of error to display
|
1290
1407
|
suggestion: Optional suggestion message to display
|
1291
1408
|
"""
|
1292
|
-
|
1293
|
-
print(getattr(e, "message", str(e)))
|
1294
|
-
if hasattr(e, "details") and e.details:
|
1295
|
-
print("\nDetails:")
|
1296
|
-
print(e.details)
|
1297
|
-
if suggestion:
|
1298
|
-
print(f"\n{suggestion}")
|
1409
|
+
handle_error(e, error_type, suggestion)
|
1299
1410
|
|
1300
1411
|
def _log_agent_load(self, agent_name: str) -> None:
|
1301
|
-
|
1302
|
-
|
1303
|
-
data={
|
1304
|
-
"progress_action": ProgressAction.LOADED,
|
1305
|
-
"agent_name": agent_name,
|
1306
|
-
},
|
1307
|
-
)
|
1412
|
+
# Using the imported function
|
1413
|
+
log_agent_load(self.app, agent_name)
|