fast-agent-mcp 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fast_agent_mcp-0.1.3.dist-info → fast_agent_mcp-0.1.5.dist-info}/METADATA +5 -1
- {fast_agent_mcp-0.1.3.dist-info → fast_agent_mcp-0.1.5.dist-info}/RECORD +28 -17
- mcp_agent/agents/agent.py +46 -0
- mcp_agent/core/agent_app.py +373 -9
- mcp_agent/core/decorators.py +455 -0
- mcp_agent/core/enhanced_prompt.py +70 -4
- mcp_agent/core/factory.py +501 -0
- mcp_agent/core/fastagent.py +140 -1059
- mcp_agent/core/proxies.py +83 -47
- mcp_agent/core/validation.py +221 -0
- mcp_agent/human_input/handler.py +5 -2
- mcp_agent/mcp/mcp_aggregator.py +537 -47
- mcp_agent/mcp/mcp_connection_manager.py +13 -2
- mcp_agent/mcp_server/__init__.py +4 -0
- mcp_agent/mcp_server/agent_server.py +121 -0
- mcp_agent/resources/examples/internal/fastagent.config.yaml +52 -0
- mcp_agent/resources/examples/internal/prompt_category.py +21 -0
- mcp_agent/resources/examples/internal/prompt_sizing.py +53 -0
- mcp_agent/resources/examples/internal/sizer.py +24 -0
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +14 -1
- mcp_agent/resources/examples/workflows/sse.py +23 -0
- mcp_agent/ui/console_display.py +278 -0
- mcp_agent/workflows/llm/augmented_llm.py +245 -179
- mcp_agent/workflows/llm/augmented_llm_anthropic.py +49 -3
- mcp_agent/workflows/llm/augmented_llm_openai.py +52 -4
- {fast_agent_mcp-0.1.3.dist-info → fast_agent_mcp-0.1.5.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.3.dist-info → fast_agent_mcp-0.1.5.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.3.dist-info → fast_agent_mcp-0.1.5.dist-info}/licenses/LICENSE +0 -0
mcp_agent/core/fastagent.py
CHANGED
@@ -3,46 +3,27 @@ Decorator-based interface for MCP Agent applications.
|
|
3
3
|
Provides a simplified way to create and manage agents using decorators.
|
4
4
|
"""
|
5
5
|
|
6
|
+
import asyncio
|
6
7
|
from typing import (
|
7
|
-
List,
|
8
8
|
Optional,
|
9
9
|
Dict,
|
10
|
-
Callable,
|
11
10
|
TypeVar,
|
12
11
|
Any,
|
13
|
-
Literal,
|
14
12
|
)
|
15
13
|
import yaml
|
16
14
|
import argparse
|
17
15
|
from contextlib import asynccontextmanager
|
16
|
+
from functools import partial
|
18
17
|
|
19
18
|
from mcp_agent.app import MCPApp
|
20
|
-
from mcp_agent.agents.agent import Agent, AgentConfig
|
21
19
|
from mcp_agent.context_dependent import ContextDependent
|
22
20
|
from mcp_agent.config import Settings
|
23
|
-
from mcp_agent.event_progress import ProgressAction
|
24
|
-
from mcp_agent.workflows.evaluator_optimizer.evaluator_optimizer import (
|
25
|
-
EvaluatorOptimizerLLM,
|
26
|
-
QualityRating,
|
27
|
-
)
|
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
|
33
21
|
|
34
22
|
from mcp_agent.core.agent_app import AgentApp
|
35
23
|
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
24
|
from mcp_agent.core.error_handling import handle_error
|
38
|
-
from mcp_agent.core.proxies import
|
39
|
-
|
40
|
-
LLMAgentProxy,
|
41
|
-
WorkflowProxy,
|
42
|
-
RouterProxy,
|
43
|
-
ChainProxy,
|
44
|
-
)
|
45
|
-
from mcp_agent.core.types import AgentOrWorkflow, ProxyDict
|
25
|
+
from mcp_agent.core.proxies import LLMAgentProxy
|
26
|
+
from mcp_agent.core.types import ProxyDict
|
46
27
|
from mcp_agent.core.exceptions import (
|
47
28
|
AgentConfigError,
|
48
29
|
CircularDependencyError,
|
@@ -52,12 +33,34 @@ from mcp_agent.core.exceptions import (
|
|
52
33
|
ProviderKeyError,
|
53
34
|
ServerInitializationError,
|
54
35
|
)
|
36
|
+
from mcp_agent.core.decorators import (
|
37
|
+
_create_decorator,
|
38
|
+
agent,
|
39
|
+
orchestrator,
|
40
|
+
parallel,
|
41
|
+
evaluator_optimizer,
|
42
|
+
router,
|
43
|
+
chain,
|
44
|
+
passthrough,
|
45
|
+
)
|
46
|
+
from mcp_agent.core.validation import (
|
47
|
+
validate_server_references,
|
48
|
+
validate_workflow_references,
|
49
|
+
)
|
50
|
+
from mcp_agent.core.factory import (
|
51
|
+
get_model_factory,
|
52
|
+
create_basic_agents,
|
53
|
+
create_agents_in_dependency_order,
|
54
|
+
create_agents_by_type,
|
55
|
+
)
|
55
56
|
|
56
57
|
# TODO -- reinstate once Windows&Python 3.13 platform issues are fixed
|
57
58
|
# import readline # noqa: F401
|
58
59
|
|
59
60
|
from rich import print
|
60
61
|
|
62
|
+
from mcp_agent.mcp_server import AgentMCPServer
|
63
|
+
|
61
64
|
T = TypeVar("T") # For the wrapper classes
|
62
65
|
|
63
66
|
|
@@ -113,64 +116,17 @@ class FastAgent(ContextDependent):
|
|
113
116
|
)
|
114
117
|
self.agents: Dict[str, Dict[str, Any]] = {}
|
115
118
|
|
116
|
-
|
117
|
-
self
|
118
|
-
|
119
|
-
|
119
|
+
# Bind decorator methods to this instance
|
120
|
+
self._create_decorator = _create_decorator.__get__(self)
|
121
|
+
self.agent = agent.__get__(self)
|
122
|
+
self.orchestrator = orchestrator.__get__(self)
|
123
|
+
self.parallel = parallel.__get__(self)
|
124
|
+
self.evaluator_optimizer = evaluator_optimizer.__get__(self)
|
125
|
+
self.router = router.__get__(self)
|
126
|
+
self.chain = chain.__get__(self)
|
127
|
+
self.passthrough = passthrough.__get__(self)
|
120
128
|
|
121
|
-
|
122
|
-
name: Name of the agent/workflow
|
123
|
-
instance: The agent or workflow instance
|
124
|
-
agent_type: Type from AgentType enum values
|
125
|
-
|
126
|
-
Returns:
|
127
|
-
Appropriate proxy type wrapping the instance
|
128
|
-
|
129
|
-
Raises:
|
130
|
-
TypeError: If instance type doesn't match expected type for agent_type
|
131
|
-
"""
|
132
|
-
if agent_type not in [
|
133
|
-
AgentType.PARALLEL.value,
|
134
|
-
AgentType.EVALUATOR_OPTIMIZER.value,
|
135
|
-
AgentType.CHAIN.value,
|
136
|
-
]:
|
137
|
-
log_agent_load(self.app, name)
|
138
|
-
if agent_type == AgentType.BASIC.value:
|
139
|
-
if not isinstance(instance, Agent):
|
140
|
-
raise TypeError(
|
141
|
-
f"Expected Agent instance for {name}, got {type(instance)}"
|
142
|
-
)
|
143
|
-
return LLMAgentProxy(self.app, name, instance)
|
144
|
-
elif agent_type == AgentType.ORCHESTRATOR.value:
|
145
|
-
if not isinstance(instance, Orchestrator):
|
146
|
-
raise TypeError(
|
147
|
-
f"Expected Orchestrator instance for {name}, got {type(instance)}"
|
148
|
-
)
|
149
|
-
return WorkflowProxy(self.app, name, instance)
|
150
|
-
elif agent_type == AgentType.PARALLEL.value:
|
151
|
-
if not isinstance(instance, ParallelLLM):
|
152
|
-
raise TypeError(
|
153
|
-
f"Expected ParallelLLM instance for {name}, got {type(instance)}"
|
154
|
-
)
|
155
|
-
return WorkflowProxy(self.app, name, instance)
|
156
|
-
elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
|
157
|
-
if not isinstance(instance, EvaluatorOptimizerLLM):
|
158
|
-
raise TypeError(
|
159
|
-
f"Expected EvaluatorOptimizerLLM instance for {name}, got {type(instance)}"
|
160
|
-
)
|
161
|
-
return WorkflowProxy(self.app, name, instance)
|
162
|
-
elif agent_type == AgentType.ROUTER.value:
|
163
|
-
if not isinstance(instance, LLMRouter):
|
164
|
-
raise TypeError(
|
165
|
-
f"Expected LLMRouter instance for {name}, got {type(instance)}"
|
166
|
-
)
|
167
|
-
return RouterProxy(self.app, name, instance)
|
168
|
-
elif agent_type == AgentType.CHAIN.value:
|
169
|
-
# Chain proxy is directly returned from _create_agents_by_type
|
170
|
-
# No need for type checking as it's already a ChainProxy
|
171
|
-
return instance
|
172
|
-
else:
|
173
|
-
raise ValueError(f"Unknown agent type: {agent_type}")
|
129
|
+
# _create_proxy moved to factory.py
|
174
130
|
|
175
131
|
@property
|
176
132
|
def context(self):
|
@@ -183,125 +139,12 @@ class FastAgent(ContextDependent):
|
|
183
139
|
with open(self.config_path) as f:
|
184
140
|
self.config = yaml.safe_load(f) or {}
|
185
141
|
|
186
|
-
|
187
|
-
"""
|
188
|
-
Validate that all server references in agent configurations exist in config.
|
189
|
-
Raises ServerConfigError if any referenced servers are not defined.
|
190
|
-
"""
|
191
|
-
if not self.context.config.mcp or not self.context.config.mcp.servers:
|
192
|
-
available_servers = set()
|
193
|
-
else:
|
194
|
-
available_servers = set(self.context.config.mcp.servers.keys())
|
195
|
-
|
196
|
-
# Check each agent's server references
|
197
|
-
for name, agent_data in self.agents.items():
|
198
|
-
config = agent_data["config"]
|
199
|
-
if config.servers:
|
200
|
-
missing = [s for s in config.servers if s not in available_servers]
|
201
|
-
if missing:
|
202
|
-
raise ServerConfigError(
|
203
|
-
f"Missing server configuration for agent '{name}'",
|
204
|
-
f"The following servers are referenced but not defined in config: {', '.join(missing)}",
|
205
|
-
)
|
206
|
-
|
207
|
-
def _validate_workflow_references(self) -> None:
|
208
|
-
"""
|
209
|
-
Validate that all workflow references point to valid agents/workflows.
|
210
|
-
Also validates that referenced agents have required configuration.
|
211
|
-
Raises AgentConfigError if any validation fails.
|
212
|
-
"""
|
213
|
-
available_components = set(self.agents.keys())
|
214
|
-
|
215
|
-
for name, agent_data in self.agents.items():
|
216
|
-
agent_type = agent_data["type"]
|
217
|
-
|
218
|
-
if agent_type == AgentType.PARALLEL.value:
|
219
|
-
# Check fan_in exists
|
220
|
-
fan_in = agent_data["fan_in"]
|
221
|
-
if fan_in not in available_components:
|
222
|
-
raise AgentConfigError(
|
223
|
-
f"Parallel workflow '{name}' references non-existent fan_in component: {fan_in}"
|
224
|
-
)
|
225
|
-
|
226
|
-
# Check fan_out agents exist
|
227
|
-
fan_out = agent_data["fan_out"]
|
228
|
-
missing = [a for a in fan_out if a not in available_components]
|
229
|
-
if missing:
|
230
|
-
raise AgentConfigError(
|
231
|
-
f"Parallel workflow '{name}' references non-existent fan_out components: {', '.join(missing)}"
|
232
|
-
)
|
233
|
-
|
234
|
-
elif agent_type == AgentType.ORCHESTRATOR.value:
|
235
|
-
# Check all child agents exist and are properly configured
|
236
|
-
child_agents = agent_data["child_agents"]
|
237
|
-
missing = [a for a in child_agents if a not in available_components]
|
238
|
-
if missing:
|
239
|
-
raise AgentConfigError(
|
240
|
-
f"Orchestrator '{name}' references non-existent agents: {', '.join(missing)}"
|
241
|
-
)
|
242
|
-
|
243
|
-
# Validate child agents have required LLM configuration
|
244
|
-
for agent_name in child_agents:
|
245
|
-
child_data = self.agents[agent_name]
|
246
|
-
if child_data["type"] == AgentType.BASIC.value:
|
247
|
-
# For basic agents, we'll validate LLM config during creation
|
248
|
-
continue
|
249
|
-
# Check if it's a workflow type or has LLM capability
|
250
|
-
# Workflows like EvaluatorOptimizer and Parallel are valid for orchestrator
|
251
|
-
func = child_data["func"]
|
252
|
-
workflow_types = [
|
253
|
-
AgentType.EVALUATOR_OPTIMIZER.value,
|
254
|
-
AgentType.PARALLEL.value,
|
255
|
-
AgentType.ROUTER.value,
|
256
|
-
AgentType.CHAIN.value,
|
257
|
-
]
|
258
|
-
|
259
|
-
if not (
|
260
|
-
isinstance(func, AugmentedLLM)
|
261
|
-
or child_data["type"] in workflow_types
|
262
|
-
or (hasattr(func, "_llm") and func._llm is not None)
|
263
|
-
):
|
264
|
-
raise AgentConfigError(
|
265
|
-
f"Agent '{agent_name}' used by orchestrator '{name}' lacks LLM capability",
|
266
|
-
"All agents used by orchestrators must be LLM-capable (either an AugmentedLLM or have an _llm property)",
|
267
|
-
)
|
268
|
-
|
269
|
-
elif agent_type == AgentType.ROUTER.value:
|
270
|
-
# Check all referenced agents exist
|
271
|
-
router_agents = agent_data["agents"]
|
272
|
-
missing = [a for a in router_agents if a not in available_components]
|
273
|
-
if missing:
|
274
|
-
raise AgentConfigError(
|
275
|
-
f"Router '{name}' references non-existent agents: {', '.join(missing)}"
|
276
|
-
)
|
277
|
-
|
278
|
-
elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
|
279
|
-
# Check both evaluator and optimizer exist
|
280
|
-
evaluator = agent_data["evaluator"]
|
281
|
-
generator = agent_data["generator"]
|
282
|
-
missing = []
|
283
|
-
if evaluator not in available_components:
|
284
|
-
missing.append(f"evaluator: {evaluator}")
|
285
|
-
if generator not in available_components:
|
286
|
-
missing.append(f"generator: {generator}")
|
287
|
-
if missing:
|
288
|
-
raise AgentConfigError(
|
289
|
-
f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
|
290
|
-
)
|
291
|
-
|
292
|
-
elif agent_type == AgentType.CHAIN.value:
|
293
|
-
# Check that all agents in the sequence exist
|
294
|
-
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
295
|
-
missing = [a for a in sequence if a not in available_components]
|
296
|
-
if missing:
|
297
|
-
raise AgentConfigError(
|
298
|
-
f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
|
299
|
-
)
|
142
|
+
# Validation methods moved to validation.py
|
300
143
|
|
301
144
|
def _get_model_factory(
|
302
145
|
self,
|
303
146
|
model: Optional[str] = None,
|
304
|
-
request_params: Optional[
|
147
|
+
request_params: Optional[Any] = None,
|
305
148
|
) -> Any:
|
306
149
|
"""
|
307
150
|
Get model factory using specified or default model.
|
@@ -314,462 +157,13 @@ class FastAgent(ContextDependent):
|
|
314
157
|
Returns:
|
315
158
|
ModelFactory instance for the specified or default model
|
316
159
|
"""
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
# Command line override has next precedence
|
322
|
-
if self.args.model:
|
323
|
-
model_spec = self.args.model
|
324
|
-
|
325
|
-
# Model from decorator has highest precedence
|
326
|
-
if model:
|
327
|
-
model_spec = model
|
328
|
-
|
329
|
-
# Update or create request_params with the final model choice
|
330
|
-
if request_params:
|
331
|
-
request_params = request_params.model_copy(update={"model": model_spec})
|
332
|
-
else:
|
333
|
-
request_params = RequestParams(model=model_spec)
|
334
|
-
|
335
|
-
# Let model factory handle the model string parsing and setup
|
336
|
-
return ModelFactory.create_factory(model_spec, request_params=request_params)
|
337
|
-
|
338
|
-
def _create_decorator(
|
339
|
-
self,
|
340
|
-
agent_type: AgentType,
|
341
|
-
default_name: str = None,
|
342
|
-
default_instruction: str = None,
|
343
|
-
default_servers: List[str] = None,
|
344
|
-
default_use_history: bool = True,
|
345
|
-
wrapper_needed: bool = False,
|
346
|
-
**extra_defaults,
|
347
|
-
) -> Callable:
|
348
|
-
"""
|
349
|
-
Factory method for creating agent decorators with common behavior.
|
350
|
-
|
351
|
-
Args:
|
352
|
-
agent_type: Type of agent/workflow to create
|
353
|
-
default_name: Default name to use if not provided
|
354
|
-
default_instruction: Default instruction to use if not provided
|
355
|
-
default_servers: Default servers list to use if not provided
|
356
|
-
default_use_history: Default history setting
|
357
|
-
wrapper_needed: Whether to wrap the decorated function
|
358
|
-
**extra_defaults: Additional agent/workflow-specific parameters
|
359
|
-
"""
|
360
|
-
|
361
|
-
def decorator_wrapper(**kwargs):
|
362
|
-
# Apply defaults for common parameters
|
363
|
-
name = kwargs.get("name", default_name or f"{agent_type.name.title()}")
|
364
|
-
instruction = kwargs.get("instruction", default_instruction or "")
|
365
|
-
servers = kwargs.get("servers", default_servers or [])
|
366
|
-
model = kwargs.get("model", None)
|
367
|
-
use_history = kwargs.get("use_history", default_use_history)
|
368
|
-
request_params = kwargs.get("request_params", None)
|
369
|
-
human_input = kwargs.get("human_input", False)
|
370
|
-
|
371
|
-
# Create base request params
|
372
|
-
def decorator(func: Callable) -> Callable:
|
373
|
-
# Create base request params
|
374
|
-
if (
|
375
|
-
request_params is not None
|
376
|
-
or model is not None
|
377
|
-
or use_history != default_use_history
|
378
|
-
):
|
379
|
-
max_tokens = 4096 if agent_type == AgentType.BASIC else None
|
380
|
-
params_dict = {"use_history": use_history, "model": model}
|
381
|
-
if max_tokens:
|
382
|
-
params_dict["maxTokens"] = max_tokens
|
383
|
-
if request_params:
|
384
|
-
params_dict.update(request_params)
|
385
|
-
base_params = RequestParams(**params_dict)
|
386
|
-
else:
|
387
|
-
base_params = RequestParams(use_history=use_history)
|
388
|
-
|
389
|
-
# Create agent configuration
|
390
|
-
config = AgentConfig(
|
391
|
-
name=name,
|
392
|
-
instruction=instruction,
|
393
|
-
servers=servers,
|
394
|
-
model=model,
|
395
|
-
use_history=use_history,
|
396
|
-
default_request_params=base_params,
|
397
|
-
human_input=human_input,
|
398
|
-
)
|
399
|
-
|
400
|
-
# Build agent/workflow specific data
|
401
|
-
agent_data = {
|
402
|
-
"config": config,
|
403
|
-
"type": agent_type.value,
|
404
|
-
"func": func,
|
405
|
-
}
|
406
|
-
|
407
|
-
# Add extra parameters specific to this agent type
|
408
|
-
for key, value in kwargs.items():
|
409
|
-
if key not in [
|
410
|
-
"name",
|
411
|
-
"instruction",
|
412
|
-
"servers",
|
413
|
-
"model",
|
414
|
-
"use_history",
|
415
|
-
"request_params",
|
416
|
-
"human_input",
|
417
|
-
]:
|
418
|
-
agent_data[key] = value
|
419
|
-
|
420
|
-
# Store the configuration under the agent name
|
421
|
-
self.agents[name] = agent_data
|
422
|
-
|
423
|
-
# Either wrap or return the original function
|
424
|
-
if wrapper_needed:
|
425
|
-
|
426
|
-
async def wrapper(*args, **kwargs):
|
427
|
-
return await func(*args, **kwargs)
|
428
|
-
|
429
|
-
return wrapper
|
430
|
-
return func
|
431
|
-
|
432
|
-
return decorator
|
433
|
-
|
434
|
-
return decorator_wrapper
|
435
|
-
|
436
|
-
def agent(
|
437
|
-
self,
|
438
|
-
name: str = "Agent",
|
439
|
-
instruction_or_kwarg: str = None,
|
440
|
-
*,
|
441
|
-
instruction: str = "You are a helpful agent.",
|
442
|
-
servers: List[str] = [],
|
443
|
-
model: str | None = None,
|
444
|
-
use_history: bool = True,
|
445
|
-
request_params: Optional[Dict] = None,
|
446
|
-
human_input: bool = False,
|
447
|
-
) -> Callable:
|
448
|
-
"""
|
449
|
-
Decorator to create and register an agent with configuration.
|
450
|
-
|
451
|
-
Args:
|
452
|
-
name: Name of the agent
|
453
|
-
instruction_or_kwarg: Optional positional parameter for instruction
|
454
|
-
instruction: Base instruction for the agent (keyword arg)
|
455
|
-
servers: List of server names the agent should connect to
|
456
|
-
model: Model specification string (highest precedence)
|
457
|
-
use_history: Whether to maintain conversation history
|
458
|
-
request_params: Additional request parameters for the LLM
|
459
|
-
human_input: Whether to enable human input capabilities
|
460
|
-
|
461
|
-
The instruction can be provided either as a second positional argument
|
462
|
-
or as a keyword argument. Positional argument takes precedence when both are provided.
|
463
|
-
|
464
|
-
Usage:
|
465
|
-
@fast.agent("agent_name", "Your instruction here") # Using positional arg
|
466
|
-
@fast.agent("agent_name", instruction="Your instruction here") # Using keyword arg
|
467
|
-
"""
|
468
|
-
# Use positional argument if provided, otherwise use keyword argument
|
469
|
-
final_instruction = (
|
470
|
-
instruction_or_kwarg if instruction_or_kwarg is not None else instruction
|
471
|
-
)
|
472
|
-
|
473
|
-
decorator = self._create_decorator(
|
474
|
-
AgentType.BASIC,
|
475
|
-
default_name="Agent",
|
476
|
-
default_instruction="You are a helpful agent.",
|
477
|
-
default_use_history=True,
|
478
|
-
)(
|
479
|
-
name=name,
|
480
|
-
instruction=final_instruction,
|
481
|
-
servers=servers,
|
482
|
-
model=model,
|
483
|
-
use_history=use_history,
|
484
|
-
request_params=request_params,
|
485
|
-
human_input=human_input,
|
486
|
-
)
|
487
|
-
return decorator
|
488
|
-
|
489
|
-
def orchestrator(
|
490
|
-
self,
|
491
|
-
name: str = "Orchestrator",
|
492
|
-
*,
|
493
|
-
instruction: str | None = None,
|
494
|
-
agents: List[str],
|
495
|
-
model: str | None = None,
|
496
|
-
use_history: bool = False,
|
497
|
-
request_params: Optional[Dict] = None,
|
498
|
-
human_input: bool = False,
|
499
|
-
plan_type: Literal["full", "iterative"] = "full",
|
500
|
-
max_iterations: int = 30, # Add the max_iterations parameter with default value
|
501
|
-
) -> Callable:
|
502
|
-
"""
|
503
|
-
Decorator to create and register an orchestrator.
|
504
|
-
|
505
|
-
Args:
|
506
|
-
name: Name of the orchestrator
|
507
|
-
instruction: Base instruction for the orchestrator
|
508
|
-
agents: List of agent names this orchestrator can use
|
509
|
-
model: Model specification string (highest precedence)
|
510
|
-
use_history: Whether to maintain conversation history (forced false)
|
511
|
-
request_params: Additional request parameters for the LLM
|
512
|
-
human_input: Whether to enable human input capabilities
|
513
|
-
plan_type: Planning approach - "full" generates entire plan first, "iterative" plans one step at a time
|
514
|
-
max_iterations: Maximum number of planning iterations (default: 10)
|
515
|
-
"""
|
516
|
-
default_instruction = """
|
517
|
-
You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
|
518
|
-
or Agents (which are collections of servers), your job is to break down the objective into a series of steps,
|
519
|
-
which can be performed by LLMs with access to the servers or agents.
|
520
|
-
"""
|
521
|
-
|
522
|
-
# Handle request_params update with max_iterations
|
523
|
-
if request_params is None:
|
524
|
-
request_params = {"max_iterations": max_iterations}
|
525
|
-
elif isinstance(request_params, dict):
|
526
|
-
if "max_iterations" not in request_params:
|
527
|
-
request_params["max_iterations"] = max_iterations
|
528
|
-
|
529
|
-
decorator = self._create_decorator(
|
530
|
-
AgentType.ORCHESTRATOR,
|
531
|
-
default_name="Orchestrator",
|
532
|
-
default_instruction=default_instruction,
|
533
|
-
default_servers=[],
|
534
|
-
default_use_history=False,
|
535
|
-
)(
|
536
|
-
name=name,
|
537
|
-
instruction=instruction,
|
538
|
-
child_agents=agents,
|
160
|
+
# Wrap the factory function to use our context and CLI model
|
161
|
+
return get_model_factory(
|
162
|
+
self.context,
|
539
163
|
model=model,
|
540
|
-
use_history=use_history,
|
541
164
|
request_params=request_params,
|
542
|
-
|
543
|
-
plan_type=plan_type,
|
165
|
+
cli_model=self.args.model if hasattr(self, "args") else None,
|
544
166
|
)
|
545
|
-
return decorator
|
546
|
-
|
547
|
-
def parallel(
|
548
|
-
self,
|
549
|
-
name: str,
|
550
|
-
fan_out: List[str],
|
551
|
-
fan_in: Optional[str] = None,
|
552
|
-
instruction: str = "",
|
553
|
-
model: str | None = None,
|
554
|
-
use_history: bool = True,
|
555
|
-
request_params: Optional[Dict] = None,
|
556
|
-
include_request: bool = True,
|
557
|
-
) -> Callable:
|
558
|
-
"""
|
559
|
-
Decorator to create and register a parallel executing agent.
|
560
|
-
|
561
|
-
Args:
|
562
|
-
name: Name of the parallel executing agent
|
563
|
-
fan_out: List of parallel execution agents
|
564
|
-
fan_in: Optional name of collecting agent. If not provided, a passthrough agent
|
565
|
-
will be created automatically with the name "{name}_fan_in"
|
566
|
-
instruction: Optional instruction for the parallel agent
|
567
|
-
model: Model specification string
|
568
|
-
use_history: Whether to maintain conversation history
|
569
|
-
request_params: Additional request parameters for the LLM
|
570
|
-
include_request: Whether to include the original request in the fan-in message
|
571
|
-
"""
|
572
|
-
# If fan_in is not provided, create a passthrough agent with a derived name
|
573
|
-
if fan_in is None:
|
574
|
-
passthrough_name = f"{name}_fan_in"
|
575
|
-
|
576
|
-
# Register the passthrough agent directly in self.agents
|
577
|
-
self.agents[passthrough_name] = {
|
578
|
-
"config": AgentConfig(
|
579
|
-
name=passthrough_name,
|
580
|
-
instruction=f"Passthrough fan-in for {name}",
|
581
|
-
servers=[],
|
582
|
-
use_history=use_history,
|
583
|
-
),
|
584
|
-
"type": AgentType.BASIC.value, # Using BASIC type since we're just attaching a PassthroughLLM
|
585
|
-
"func": lambda x: x, # Simple passthrough function (never actually called)
|
586
|
-
}
|
587
|
-
|
588
|
-
# Use this passthrough as the fan-in
|
589
|
-
fan_in = passthrough_name
|
590
|
-
|
591
|
-
decorator = self._create_decorator(
|
592
|
-
AgentType.PARALLEL,
|
593
|
-
default_instruction="",
|
594
|
-
default_servers=[],
|
595
|
-
default_use_history=True,
|
596
|
-
)(
|
597
|
-
name=name,
|
598
|
-
fan_in=fan_in,
|
599
|
-
fan_out=fan_out,
|
600
|
-
instruction=instruction,
|
601
|
-
model=model,
|
602
|
-
use_history=use_history,
|
603
|
-
request_params=request_params,
|
604
|
-
include_request=include_request,
|
605
|
-
)
|
606
|
-
return decorator
|
607
|
-
|
608
|
-
def evaluator_optimizer(
|
609
|
-
self,
|
610
|
-
name: str,
|
611
|
-
generator: str,
|
612
|
-
evaluator: str,
|
613
|
-
min_rating: str = "GOOD",
|
614
|
-
max_refinements: int = 3,
|
615
|
-
use_history: bool = True,
|
616
|
-
request_params: Optional[Dict] = None,
|
617
|
-
instruction: Optional[str] = None,
|
618
|
-
) -> Callable:
|
619
|
-
"""
|
620
|
-
Decorator to create and register an evaluator-optimizer workflow.
|
621
|
-
|
622
|
-
Args:
|
623
|
-
name: Name of the workflow
|
624
|
-
generator: Name of the generator agent
|
625
|
-
evaluator: Name of the evaluator agent
|
626
|
-
min_rating: Minimum acceptable quality rating (EXCELLENT, GOOD, FAIR, POOR)
|
627
|
-
max_refinements: Maximum number of refinement iterations
|
628
|
-
use_history: Whether to maintain conversation history
|
629
|
-
request_params: Additional request parameters for the LLM
|
630
|
-
instruction: Optional instruction for the workflow (if not provided, uses generator's instruction)
|
631
|
-
"""
|
632
|
-
decorator = self._create_decorator(
|
633
|
-
AgentType.EVALUATOR_OPTIMIZER,
|
634
|
-
default_instruction="", # We'll get instruction from generator or override
|
635
|
-
default_servers=[],
|
636
|
-
default_use_history=True,
|
637
|
-
wrapper_needed=True,
|
638
|
-
)(
|
639
|
-
name=name,
|
640
|
-
generator=generator,
|
641
|
-
evaluator=evaluator,
|
642
|
-
min_rating=min_rating,
|
643
|
-
max_refinements=max_refinements,
|
644
|
-
use_history=use_history,
|
645
|
-
request_params=request_params,
|
646
|
-
instruction=instruction, # Pass through any custom instruction
|
647
|
-
)
|
648
|
-
return decorator
|
649
|
-
|
650
|
-
def router(
|
651
|
-
self,
|
652
|
-
name: str,
|
653
|
-
agents: List[str],
|
654
|
-
# servers: List[str] = [],
|
655
|
-
model: Optional[str] = None,
|
656
|
-
use_history: bool = True,
|
657
|
-
request_params: Optional[Dict] = None,
|
658
|
-
human_input: bool = False,
|
659
|
-
) -> Callable:
|
660
|
-
"""
|
661
|
-
Decorator to create and register a router.
|
662
|
-
|
663
|
-
Args:
|
664
|
-
name: Name of the router
|
665
|
-
agents: List of agent names this router can delegate to
|
666
|
-
servers: List of server names the router can use directly (currently not supported)
|
667
|
-
model: Model specification string
|
668
|
-
use_history: Whether to maintain conversation history
|
669
|
-
request_params: Additional request parameters for the LLM
|
670
|
-
human_input: Whether to enable human input capabilities
|
671
|
-
"""
|
672
|
-
decorator = self._create_decorator(
|
673
|
-
AgentType.ROUTER,
|
674
|
-
default_instruction="",
|
675
|
-
default_servers=[],
|
676
|
-
default_use_history=False,
|
677
|
-
wrapper_needed=True,
|
678
|
-
)(
|
679
|
-
name=name,
|
680
|
-
agents=agents,
|
681
|
-
model=model,
|
682
|
-
use_history=use_history,
|
683
|
-
request_params=request_params,
|
684
|
-
human_input=human_input,
|
685
|
-
)
|
686
|
-
return decorator
|
687
|
-
|
688
|
-
def chain(
|
689
|
-
self,
|
690
|
-
name: str = "Chain",
|
691
|
-
*,
|
692
|
-
sequence: List[str] = None,
|
693
|
-
agents: List[str] = None, # Alias for sequence
|
694
|
-
instruction: str = None,
|
695
|
-
model: str | None = None,
|
696
|
-
use_history: bool = True,
|
697
|
-
request_params: Optional[Dict] = None,
|
698
|
-
continue_with_final: bool = True,
|
699
|
-
cumulative: bool = False,
|
700
|
-
) -> Callable:
|
701
|
-
"""
|
702
|
-
Decorator to create and register a chain of agents.
|
703
|
-
|
704
|
-
Args:
|
705
|
-
name: Name of the chain
|
706
|
-
sequence: List of agent names in order of execution (preferred name)
|
707
|
-
agents: Alias for sequence (backwards compatibility)
|
708
|
-
instruction: Optional custom instruction for the chain (if none provided, will autogenerate based on sequence)
|
709
|
-
model: Model specification string (not used directly in chain)
|
710
|
-
use_history: Whether to maintain conversation history
|
711
|
-
request_params: Additional request parameters
|
712
|
-
continue_with_final: When using prompt(), whether to continue with the final agent after processing chain (default: True)
|
713
|
-
cumulative: When True, each agent receives all previous agent responses concatenated (default: False)
|
714
|
-
When False, each agent only gets the output of the previous agent (default behavior)
|
715
|
-
"""
|
716
|
-
# Support both parameter names
|
717
|
-
agent_sequence = sequence or agents
|
718
|
-
if agent_sequence is None:
|
719
|
-
raise ValueError("Either 'sequence' or 'agents' parameter must be provided")
|
720
|
-
|
721
|
-
# Auto-generate instruction if not provided
|
722
|
-
if instruction is None:
|
723
|
-
# Generate an appropriate instruction based on mode
|
724
|
-
if cumulative:
|
725
|
-
instruction = f"Cumulative chain of agents: {', '.join(agent_sequence)}"
|
726
|
-
else:
|
727
|
-
instruction = f"Chain of agents: {', '.join(agent_sequence)}"
|
728
|
-
|
729
|
-
decorator = self._create_decorator(
|
730
|
-
AgentType.CHAIN,
|
731
|
-
default_name="Chain",
|
732
|
-
default_instruction=instruction,
|
733
|
-
default_use_history=True,
|
734
|
-
wrapper_needed=True,
|
735
|
-
)(
|
736
|
-
name=name,
|
737
|
-
sequence=agent_sequence,
|
738
|
-
instruction=instruction,
|
739
|
-
model=model,
|
740
|
-
use_history=use_history,
|
741
|
-
request_params=request_params,
|
742
|
-
continue_with_final=continue_with_final,
|
743
|
-
cumulative=cumulative,
|
744
|
-
)
|
745
|
-
return decorator
|
746
|
-
|
747
|
-
def passthrough(
|
748
|
-
self, name: str = "Passthrough", use_history: bool = True, **kwargs
|
749
|
-
) -> Callable:
|
750
|
-
"""
|
751
|
-
Decorator to create and register a passthrough agent.
|
752
|
-
A passthrough agent simply returns any input message without modification.
|
753
|
-
|
754
|
-
This is useful for parallel workflows where no fan-in aggregation is needed
|
755
|
-
(the fan-in agent can be a passthrough that simply returns the combined outputs).
|
756
|
-
|
757
|
-
Args:
|
758
|
-
name: Name of the passthrough agent
|
759
|
-
use_history: Whether to maintain conversation history
|
760
|
-
**kwargs: Additional parameters (ignored, for compatibility)
|
761
|
-
"""
|
762
|
-
decorator = self._create_decorator(
|
763
|
-
AgentType.BASIC, # Using BASIC agent type since we'll use a regular agent with PassthroughLLM
|
764
|
-
default_name="Passthrough",
|
765
|
-
default_instruction="Passthrough agent that returns input without modification",
|
766
|
-
default_use_history=use_history,
|
767
|
-
wrapper_needed=True,
|
768
|
-
)(
|
769
|
-
name=name,
|
770
|
-
use_history=use_history,
|
771
|
-
)
|
772
|
-
return decorator
|
773
167
|
|
774
168
|
async def _create_agents_by_type(
|
775
169
|
self,
|
@@ -790,261 +184,17 @@ class FastAgent(ContextDependent):
|
|
790
184
|
Returns:
|
791
185
|
Dictionary of initialized agents wrapped in appropriate proxies
|
792
186
|
"""
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
# Create a dictionary to store the initialized agents
|
797
|
-
result_agents = {}
|
798
|
-
|
799
|
-
# Get all agents of the specified type
|
800
|
-
for name, agent_data in self.agents.items():
|
801
|
-
if agent_data["type"] == agent_type.value:
|
802
|
-
# Get common configuration
|
803
|
-
config = agent_data["config"]
|
804
|
-
|
805
|
-
# Type-specific initialization
|
806
|
-
if agent_type == AgentType.BASIC:
|
807
|
-
# Get the agent name for special handling
|
808
|
-
agent_name = agent_data["config"].name
|
809
|
-
|
810
|
-
# Check if this is an agent that should use the PassthroughLLM
|
811
|
-
if agent_name.endswith("_fan_in") or agent_name.startswith(
|
812
|
-
"passthrough"
|
813
|
-
):
|
814
|
-
# Import here to avoid circular imports
|
815
|
-
from mcp_agent.workflows.llm.augmented_llm import PassthroughLLM
|
816
|
-
|
817
|
-
# Create basic agent with configuration
|
818
|
-
agent = Agent(config=config, context=agent_app.context)
|
819
|
-
|
820
|
-
# Set up a PassthroughLLM directly
|
821
|
-
async with agent:
|
822
|
-
agent._llm = PassthroughLLM(
|
823
|
-
name=f"{config.name}_llm",
|
824
|
-
context=agent_app.context,
|
825
|
-
agent=agent,
|
826
|
-
default_request_params=config.default_request_params,
|
827
|
-
)
|
828
|
-
|
829
|
-
# Store the agent
|
830
|
-
instance = agent
|
831
|
-
else:
|
832
|
-
# Standard basic agent with LLM
|
833
|
-
agent = Agent(config=config, context=agent_app.context)
|
834
|
-
|
835
|
-
# Set up LLM with proper configuration
|
836
|
-
async with agent:
|
837
|
-
llm_factory = self._get_model_factory(
|
838
|
-
model=config.model,
|
839
|
-
request_params=config.default_request_params,
|
840
|
-
)
|
841
|
-
agent._llm = await agent.attach_llm(llm_factory)
|
842
|
-
|
843
|
-
# Store the agent
|
844
|
-
instance = agent
|
845
|
-
|
846
|
-
elif agent_type == AgentType.ORCHESTRATOR:
|
847
|
-
# Get base params configured with model settings
|
848
|
-
base_params = (
|
849
|
-
config.default_request_params.model_copy()
|
850
|
-
if config.default_request_params
|
851
|
-
else RequestParams()
|
852
|
-
)
|
853
|
-
base_params.use_history = False # Force no history for orchestrator
|
854
|
-
|
855
|
-
# Get the child agents - need to unwrap proxies and validate LLM config
|
856
|
-
child_agents = []
|
857
|
-
for agent_name in agent_data["child_agents"]:
|
858
|
-
proxy = active_agents[agent_name]
|
859
|
-
instance = self._unwrap_proxy(proxy)
|
860
|
-
# Validate basic agents have LLM
|
861
|
-
if isinstance(instance, Agent):
|
862
|
-
if not hasattr(instance, "_llm") or not instance._llm:
|
863
|
-
raise AgentConfigError(
|
864
|
-
f"Agent '{agent_name}' used by orchestrator '{name}' missing LLM configuration",
|
865
|
-
"All agents must be fully configured with LLMs before being used in an orchestrator",
|
866
|
-
)
|
867
|
-
child_agents.append(instance)
|
868
|
-
|
869
|
-
# Create a properly configured planner agent
|
870
|
-
planner_config = AgentConfig(
|
871
|
-
name=f"{name}", # Use orchestrator name as prefix
|
872
|
-
instruction=config.instruction
|
873
|
-
or """
|
874
|
-
You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
|
875
|
-
or Agents (which are collections of servers), your job is to break down the objective into a series of steps,
|
876
|
-
which can be performed by LLMs with access to the servers or agents.
|
877
|
-
""",
|
878
|
-
servers=[], # Planner doesn't need server access
|
879
|
-
model=config.model,
|
880
|
-
default_request_params=base_params,
|
881
|
-
)
|
882
|
-
planner_agent = Agent(
|
883
|
-
config=planner_config, context=agent_app.context
|
884
|
-
)
|
885
|
-
planner_factory = self._get_model_factory(
|
886
|
-
model=config.model,
|
887
|
-
request_params=config.default_request_params,
|
888
|
-
)
|
889
|
-
|
890
|
-
async with planner_agent:
|
891
|
-
planner = await planner_agent.attach_llm(planner_factory)
|
892
|
-
|
893
|
-
# Create the orchestrator with pre-configured planner
|
894
|
-
instance = Orchestrator(
|
895
|
-
name=config.name,
|
896
|
-
planner=planner, # Pass pre-configured planner
|
897
|
-
available_agents=child_agents,
|
898
|
-
context=agent_app.context,
|
899
|
-
request_params=planner.default_request_params, # Base params already include model settings
|
900
|
-
plan_type=agent_data.get(
|
901
|
-
"plan_type", "full"
|
902
|
-
), # Get plan_type from agent_data
|
903
|
-
verb=ProgressAction.PLANNING,
|
904
|
-
)
|
905
|
-
|
906
|
-
elif agent_type == AgentType.EVALUATOR_OPTIMIZER:
|
907
|
-
# Get the referenced agents - unwrap from proxies
|
908
|
-
generator = self._unwrap_proxy(
|
909
|
-
active_agents[agent_data["generator"]]
|
910
|
-
)
|
911
|
-
evaluator = self._unwrap_proxy(
|
912
|
-
active_agents[agent_data["evaluator"]]
|
913
|
-
)
|
914
|
-
|
915
|
-
if not generator or not evaluator:
|
916
|
-
raise ValueError(
|
917
|
-
f"Missing agents for workflow {name}: "
|
918
|
-
f"generator={agent_data['generator']}, "
|
919
|
-
f"evaluator={agent_data['evaluator']}"
|
920
|
-
)
|
921
|
-
|
922
|
-
# Get model from generator if it's an Agent, or from config otherwise
|
923
|
-
optimizer_model = None
|
924
|
-
if isinstance(generator, Agent):
|
925
|
-
optimizer_model = generator.config.model
|
926
|
-
elif hasattr(generator, '_sequence') and hasattr(generator, '_agent_proxies'):
|
927
|
-
# For ChainProxy, use the config model directly
|
928
|
-
optimizer_model = config.model
|
929
|
-
|
930
|
-
instance = EvaluatorOptimizerLLM(
|
931
|
-
name=config.name, # Pass name from config
|
932
|
-
generator=generator,
|
933
|
-
evaluator=evaluator,
|
934
|
-
min_rating=QualityRating[agent_data["min_rating"]],
|
935
|
-
max_refinements=agent_data["max_refinements"],
|
936
|
-
llm_factory=self._get_model_factory(model=optimizer_model),
|
937
|
-
context=agent_app.context,
|
938
|
-
instruction=config.instruction, # Pass any custom instruction
|
939
|
-
)
|
940
|
-
|
941
|
-
elif agent_type == AgentType.ROUTER:
|
942
|
-
# Get the router's agents - unwrap proxies
|
943
|
-
router_agents = self._get_agent_instances(
|
944
|
-
agent_data["agents"], active_agents
|
945
|
-
)
|
946
|
-
|
947
|
-
# Create the router with proper configuration
|
948
|
-
llm_factory = self._get_model_factory(
|
949
|
-
model=config.model,
|
950
|
-
request_params=config.default_request_params,
|
951
|
-
)
|
952
|
-
|
953
|
-
instance = LLMRouter(
|
954
|
-
name=config.name,
|
955
|
-
llm_factory=llm_factory,
|
956
|
-
agents=router_agents,
|
957
|
-
server_names=config.servers,
|
958
|
-
context=agent_app.context,
|
959
|
-
default_request_params=config.default_request_params,
|
960
|
-
verb=ProgressAction.ROUTING, # Set verb for progress display
|
961
|
-
)
|
962
|
-
|
963
|
-
elif agent_type == AgentType.CHAIN:
|
964
|
-
# Get the sequence from either parameter
|
965
|
-
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
966
|
-
|
967
|
-
# Auto-generate instruction if not provided or if it's just the default
|
968
|
-
default_instruction = f"Chain of agents: {', '.join(sequence)}"
|
969
|
-
|
970
|
-
# If user provided a custom instruction, use that
|
971
|
-
# Otherwise, generate a description based on the sequence and their servers
|
972
|
-
if config.instruction == default_instruction:
|
973
|
-
# Get all agent names in the sequence
|
974
|
-
agent_names = []
|
975
|
-
all_servers = set()
|
976
|
-
|
977
|
-
# Collect information about the agents and their servers
|
978
|
-
for agent_name in sequence:
|
979
|
-
if agent_name in active_agents:
|
980
|
-
agent_proxy = active_agents[agent_name]
|
981
|
-
if hasattr(agent_proxy, "_agent"):
|
982
|
-
# For LLMAgentProxy
|
983
|
-
agent_instance = agent_proxy._agent
|
984
|
-
agent_names.append(agent_name)
|
985
|
-
if hasattr(agent_instance, "server_names"):
|
986
|
-
all_servers.update(agent_instance.server_names)
|
987
|
-
elif hasattr(agent_proxy, "_workflow"):
|
988
|
-
# For WorkflowProxy
|
989
|
-
agent_names.append(agent_name)
|
990
|
-
|
991
|
-
# Generate a better description
|
992
|
-
if agent_names:
|
993
|
-
server_part = (
|
994
|
-
f" with access to servers: {', '.join(sorted(all_servers))}"
|
995
|
-
if all_servers
|
996
|
-
else ""
|
997
|
-
)
|
998
|
-
config.instruction = f"Sequence of agents: {', '.join(agent_names)}{server_part}."
|
999
|
-
|
1000
|
-
# Create a ChainProxy without needing a new instance
|
1001
|
-
# Just pass the agent proxies and sequence
|
1002
|
-
instance = ChainProxy(self.app, name, sequence, active_agents)
|
1003
|
-
# Set continue_with_final behavior from configuration
|
1004
|
-
instance._continue_with_final = agent_data.get(
|
1005
|
-
"continue_with_final", True
|
1006
|
-
)
|
1007
|
-
# Set cumulative behavior from configuration
|
1008
|
-
instance._cumulative = agent_data.get(
|
1009
|
-
"cumulative", False
|
1010
|
-
)
|
1011
|
-
|
1012
|
-
# We removed the AgentType.PASSTHROUGH case
|
1013
|
-
# Passthrough agents are now created as BASIC agents with a special LLM
|
1014
|
-
|
1015
|
-
elif agent_type == AgentType.PARALLEL:
|
1016
|
-
# Get fan-out agents (could be basic agents or other parallels)
|
1017
|
-
fan_out_agents = self._get_agent_instances(
|
1018
|
-
agent_data["fan_out"], active_agents
|
1019
|
-
)
|
1020
|
-
|
1021
|
-
# Get fan-in agent - unwrap proxy
|
1022
|
-
fan_in_agent = self._unwrap_proxy(
|
1023
|
-
active_agents[agent_data["fan_in"]]
|
1024
|
-
)
|
1025
|
-
|
1026
|
-
# Create the parallel workflow
|
1027
|
-
llm_factory = self._get_model_factory(config.model)
|
1028
|
-
instance = ParallelLLM(
|
1029
|
-
name=config.name,
|
1030
|
-
instruction=config.instruction,
|
1031
|
-
fan_out_agents=fan_out_agents,
|
1032
|
-
fan_in_agent=fan_in_agent,
|
1033
|
-
context=agent_app.context,
|
1034
|
-
llm_factory=llm_factory,
|
1035
|
-
default_request_params=config.default_request_params,
|
1036
|
-
include_request=agent_data.get("include_request", True),
|
1037
|
-
)
|
1038
|
-
|
1039
|
-
else:
|
1040
|
-
raise ValueError(f"Unsupported agent type: {agent_type}")
|
1041
|
-
|
1042
|
-
# Create the appropriate proxy and store in results
|
1043
|
-
result_agents[name] = self._create_proxy(
|
1044
|
-
name, instance, agent_type.value
|
1045
|
-
)
|
187
|
+
# Create a model factory function that we can pass to the factory module
|
188
|
+
model_factory_func = partial(self._get_model_factory)
|
1046
189
|
|
1047
|
-
return
|
190
|
+
return await create_agents_by_type(
|
191
|
+
agent_app,
|
192
|
+
self.agents,
|
193
|
+
agent_type,
|
194
|
+
active_agents,
|
195
|
+
model_factory_func=model_factory_func,
|
196
|
+
**kwargs,
|
197
|
+
)
|
1048
198
|
|
1049
199
|
async def _create_basic_agents(self, agent_app: MCPApp) -> ProxyDict:
|
1050
200
|
"""
|
@@ -1056,7 +206,8 @@ class FastAgent(ContextDependent):
|
|
1056
206
|
Returns:
|
1057
207
|
Dictionary of initialized basic agents wrapped in appropriate proxies
|
1058
208
|
"""
|
1059
|
-
|
209
|
+
model_factory_func = partial(self._get_model_factory)
|
210
|
+
return await create_basic_agents(agent_app, self.agents, model_factory_func)
|
1060
211
|
|
1061
212
|
async def _create_orchestrators(
|
1062
213
|
self, agent_app: MCPApp, active_agents: ProxyDict
|
@@ -1092,83 +243,27 @@ class FastAgent(ContextDependent):
|
|
1092
243
|
agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
|
1093
244
|
)
|
1094
245
|
|
1095
|
-
def
|
1096
|
-
self,
|
1097
|
-
) ->
|
1098
|
-
"""
|
1099
|
-
Get dependencies for an agent in topological order.
|
1100
|
-
Works for both Parallel and Chain workflows.
|
1101
|
-
|
1102
|
-
Args:
|
1103
|
-
name: Name of the agent
|
1104
|
-
visited: Set of already visited agents
|
1105
|
-
path: Current path for cycle detection
|
1106
|
-
agent_type: Optional type filter (e.g., only check Parallel or Chain)
|
1107
|
-
|
1108
|
-
Returns:
|
1109
|
-
List of agent names in dependency order
|
1110
|
-
|
1111
|
-
Raises:
|
1112
|
-
ValueError: If circular dependency detected
|
1113
|
-
"""
|
1114
|
-
if name in path:
|
1115
|
-
path_str = " -> ".join(path)
|
1116
|
-
raise CircularDependencyError(f"Path: {path_str} -> {name}")
|
1117
|
-
|
1118
|
-
if name in visited:
|
1119
|
-
return []
|
1120
|
-
|
1121
|
-
if name not in self.agents:
|
1122
|
-
return []
|
1123
|
-
|
1124
|
-
config = self.agents[name]
|
1125
|
-
|
1126
|
-
# Skip if not the requested type (when filtering)
|
1127
|
-
if agent_type and config["type"] != agent_type.value:
|
1128
|
-
return []
|
1129
|
-
|
1130
|
-
path.add(name)
|
1131
|
-
deps = []
|
1132
|
-
|
1133
|
-
# Handle dependencies based on agent type
|
1134
|
-
if config["type"] == AgentType.PARALLEL.value:
|
1135
|
-
# Get dependencies from fan-out agents
|
1136
|
-
for fan_out in config["fan_out"]:
|
1137
|
-
deps.extend(self._get_dependencies(fan_out, visited, path, agent_type))
|
1138
|
-
elif config["type"] == AgentType.CHAIN.value:
|
1139
|
-
# Get dependencies from sequence agents
|
1140
|
-
sequence = config.get("sequence", config.get("agents", []))
|
1141
|
-
for agent_name in sequence:
|
1142
|
-
deps.extend(
|
1143
|
-
self._get_dependencies(agent_name, visited, path, agent_type)
|
1144
|
-
)
|
1145
|
-
|
1146
|
-
# Add this agent after its dependencies
|
1147
|
-
deps.append(name)
|
1148
|
-
visited.add(name)
|
1149
|
-
path.remove(name)
|
1150
|
-
|
1151
|
-
return deps
|
1152
|
-
|
1153
|
-
def _get_parallel_dependencies(
|
1154
|
-
self, name: str, visited: set, path: set
|
1155
|
-
) -> List[str]:
|
246
|
+
async def _create_parallel_agents(
|
247
|
+
self, agent_app: MCPApp, active_agents: ProxyDict
|
248
|
+
) -> ProxyDict:
|
1156
249
|
"""
|
1157
|
-
|
1158
|
-
Legacy function that calls the more general _get_dependencies.
|
250
|
+
Create parallel execution agents in dependency order.
|
1159
251
|
|
1160
252
|
Args:
|
1161
|
-
|
1162
|
-
|
1163
|
-
path: Current path for cycle detection
|
253
|
+
agent_app: The main application instance
|
254
|
+
active_agents: Dictionary of already created agents/proxies
|
1164
255
|
|
1165
256
|
Returns:
|
1166
|
-
|
1167
|
-
|
1168
|
-
Raises:
|
1169
|
-
ValueError: If circular dependency detected
|
257
|
+
Dictionary of initialized parallel agents
|
1170
258
|
"""
|
1171
|
-
|
259
|
+
model_factory_func = partial(self._get_model_factory)
|
260
|
+
return await create_agents_in_dependency_order(
|
261
|
+
agent_app,
|
262
|
+
self.agents,
|
263
|
+
active_agents,
|
264
|
+
AgentType.PARALLEL,
|
265
|
+
model_factory_func,
|
266
|
+
)
|
1172
267
|
|
1173
268
|
async def _create_agents_in_dependency_order(
|
1174
269
|
self, agent_app: MCPApp, active_agents: ProxyDict, agent_type: AgentType
|
@@ -1185,59 +280,9 @@ class FastAgent(ContextDependent):
|
|
1185
280
|
Returns:
|
1186
281
|
Dictionary of initialized agents
|
1187
282
|
"""
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
# Get all agents of the specified type
|
1192
|
-
agent_names = [
|
1193
|
-
name
|
1194
|
-
for name, agent_data in self.agents.items()
|
1195
|
-
if agent_data["type"] == agent_type.value
|
1196
|
-
]
|
1197
|
-
|
1198
|
-
# Create agents in dependency order
|
1199
|
-
for name in agent_names:
|
1200
|
-
# Get ordered dependencies if not already processed
|
1201
|
-
if name not in visited:
|
1202
|
-
try:
|
1203
|
-
ordered_agents = self._get_dependencies(
|
1204
|
-
name, visited, set(), agent_type
|
1205
|
-
)
|
1206
|
-
except ValueError as e:
|
1207
|
-
raise ValueError(
|
1208
|
-
f"Error creating {agent_type.name.lower()} agent {name}: {str(e)}"
|
1209
|
-
)
|
1210
|
-
|
1211
|
-
# Create each agent in order
|
1212
|
-
for agent_name in ordered_agents:
|
1213
|
-
if agent_name not in result_agents:
|
1214
|
-
# Create one agent at a time using the generic method
|
1215
|
-
agent_result = await self._create_agents_by_type(
|
1216
|
-
agent_app,
|
1217
|
-
agent_type,
|
1218
|
-
active_agents,
|
1219
|
-
agent_name=agent_name,
|
1220
|
-
)
|
1221
|
-
if agent_name in agent_result:
|
1222
|
-
result_agents[agent_name] = agent_result[agent_name]
|
1223
|
-
|
1224
|
-
return result_agents
|
1225
|
-
|
1226
|
-
async def _create_parallel_agents(
|
1227
|
-
self, agent_app: MCPApp, active_agents: ProxyDict
|
1228
|
-
) -> ProxyDict:
|
1229
|
-
"""
|
1230
|
-
Create parallel execution agents in dependency order.
|
1231
|
-
|
1232
|
-
Args:
|
1233
|
-
agent_app: The main application instance
|
1234
|
-
active_agents: Dictionary of already created agents/proxies
|
1235
|
-
|
1236
|
-
Returns:
|
1237
|
-
Dictionary of initialized parallel agents
|
1238
|
-
"""
|
1239
|
-
return await self._create_agents_in_dependency_order(
|
1240
|
-
agent_app, active_agents, AgentType.PARALLEL
|
283
|
+
model_factory_func = partial(self._get_model_factory)
|
284
|
+
return await create_agents_in_dependency_order(
|
285
|
+
agent_app, self.agents, active_agents, agent_type, model_factory_func
|
1241
286
|
)
|
1242
287
|
|
1243
288
|
async def _create_routers(
|
@@ -1257,33 +302,6 @@ class FastAgent(ContextDependent):
|
|
1257
302
|
agent_app, AgentType.ROUTER, active_agents
|
1258
303
|
)
|
1259
304
|
|
1260
|
-
def _unwrap_proxy(self, proxy: BaseAgentProxy) -> AgentOrWorkflow:
|
1261
|
-
"""
|
1262
|
-
Unwrap a proxy to get the underlying agent or workflow instance.
|
1263
|
-
|
1264
|
-
Args:
|
1265
|
-
proxy: The proxy object to unwrap
|
1266
|
-
|
1267
|
-
Returns:
|
1268
|
-
The underlying Agent or workflow instance
|
1269
|
-
"""
|
1270
|
-
return unwrap_proxy(proxy)
|
1271
|
-
|
1272
|
-
def _get_agent_instances(
|
1273
|
-
self, agent_names: List[str], active_agents: ProxyDict
|
1274
|
-
) -> List[AgentOrWorkflow]:
|
1275
|
-
"""
|
1276
|
-
Get list of actual agent/workflow instances from a list of names.
|
1277
|
-
|
1278
|
-
Args:
|
1279
|
-
agent_names: List of agent names to look up
|
1280
|
-
active_agents: Dictionary of active agent proxies
|
1281
|
-
|
1282
|
-
Returns:
|
1283
|
-
List of unwrapped agent/workflow instances
|
1284
|
-
"""
|
1285
|
-
return get_agent_instances(agent_names, active_agents)
|
1286
|
-
|
1287
305
|
@asynccontextmanager
|
1288
306
|
async def run(self):
|
1289
307
|
"""
|
@@ -1315,8 +333,8 @@ class FastAgent(ContextDependent):
|
|
1315
333
|
progress_display.stop() # This will stop and hide the display
|
1316
334
|
|
1317
335
|
# Pre-flight validation
|
1318
|
-
self.
|
1319
|
-
self.
|
336
|
+
validate_server_references(self.context, self.agents)
|
337
|
+
validate_workflow_references(self.agents)
|
1320
338
|
|
1321
339
|
# Create all types of agents in dependency order
|
1322
340
|
# First create basic agents
|
@@ -1337,7 +355,7 @@ class FastAgent(ContextDependent):
|
|
1337
355
|
agent_app, active_agents, AgentType.CHAIN
|
1338
356
|
)
|
1339
357
|
active_agents.update(chains)
|
1340
|
-
|
358
|
+
|
1341
359
|
# Now create evaluator-optimizers AFTER chains are available
|
1342
360
|
evaluator_optimizers = await self._create_evaluator_optimizers(
|
1343
361
|
agent_app, active_agents
|
@@ -1471,5 +489,68 @@ class FastAgent(ContextDependent):
|
|
1471
489
|
handle_error(e, error_type, suggestion)
|
1472
490
|
|
1473
491
|
def _log_agent_load(self, agent_name: str) -> None:
|
1474
|
-
#
|
1475
|
-
|
492
|
+
# This function is no longer needed - agent loading is now handled in factory.py
|
493
|
+
pass
|
494
|
+
|
495
|
+
def create_mcp_server(
|
496
|
+
self,
|
497
|
+
agent_app_instance: AgentApp,
|
498
|
+
server_name: str = None,
|
499
|
+
server_description: str = None,
|
500
|
+
) -> AgentMCPServer:
|
501
|
+
"""
|
502
|
+
Create an MCP server that exposes the agents as tools.
|
503
|
+
|
504
|
+
Args:
|
505
|
+
agent_app_instance: The AgentApp instance with initialized agents
|
506
|
+
server_name: Optional custom name for the MCP server
|
507
|
+
server_description: Optional description for the MCP server
|
508
|
+
|
509
|
+
Returns:
|
510
|
+
An AgentMCPServer instance ready to be run
|
511
|
+
"""
|
512
|
+
return AgentMCPServer(
|
513
|
+
agent_app=agent_app_instance,
|
514
|
+
server_name=server_name or f"{self.name}-MCP-Server",
|
515
|
+
server_description=server_description,
|
516
|
+
)
|
517
|
+
|
518
|
+
async def run_with_mcp_server(
|
519
|
+
self,
|
520
|
+
transport: str = "sse",
|
521
|
+
host: str = "0.0.0.0",
|
522
|
+
port: int = 8000,
|
523
|
+
server_name: str = None,
|
524
|
+
server_description: str = None,
|
525
|
+
):
|
526
|
+
"""
|
527
|
+
Run the FastAgent application and expose agents through an MCP server.
|
528
|
+
|
529
|
+
Args:
|
530
|
+
transport: Transport protocol to use ("stdio" or "sse")
|
531
|
+
host: Host address for the server when using SSE
|
532
|
+
port: Port for the server when using SSE
|
533
|
+
server_name: Optional custom name for the MCP server
|
534
|
+
server_description: Optional description for the MCP server
|
535
|
+
"""
|
536
|
+
async with self.run() as agent_app:
|
537
|
+
# Create the MCP server
|
538
|
+
mcp_server = self.create_mcp_server(
|
539
|
+
agent_app_instance=agent_app,
|
540
|
+
server_name=server_name,
|
541
|
+
server_description=server_description,
|
542
|
+
)
|
543
|
+
|
544
|
+
# Run the MCP server in a separate task
|
545
|
+
server_task = asyncio.create_task(
|
546
|
+
mcp_server.run_async(transport=transport, host=host, port=port)
|
547
|
+
)
|
548
|
+
|
549
|
+
try:
|
550
|
+
# Wait for the server task to complete (or be cancelled)
|
551
|
+
await server_task
|
552
|
+
except asyncio.CancelledError:
|
553
|
+
# Propagate cancellation
|
554
|
+
server_task.cancel()
|
555
|
+
await asyncio.gather(server_task, return_exceptions=True)
|
556
|
+
raise
|