fast-agent-mcp 0.1.13__py3-none-any.whl → 0.2.0__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.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
- fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
- mcp_agent/__init__.py +75 -0
- mcp_agent/agents/agent.py +59 -371
- mcp_agent/agents/base_agent.py +522 -0
- mcp_agent/agents/workflow/__init__.py +1 -0
- mcp_agent/agents/workflow/chain_agent.py +173 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
- mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
- mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +27 -11
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +3 -1
- mcp_agent/cli/commands/bootstrap.py +18 -7
- mcp_agent/cli/commands/setup.py +12 -4
- mcp_agent/cli/main.py +1 -1
- mcp_agent/cli/terminal.py +1 -1
- mcp_agent/config.py +24 -35
- mcp_agent/context.py +3 -1
- mcp_agent/context_dependent.py +3 -1
- mcp_agent/core/agent_types.py +10 -7
- mcp_agent/core/direct_agent_app.py +179 -0
- mcp_agent/core/direct_decorators.py +443 -0
- mcp_agent/core/direct_factory.py +476 -0
- mcp_agent/core/enhanced_prompt.py +15 -20
- mcp_agent/core/fastagent.py +151 -337
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +19 -11
- mcp_agent/core/prompt.py +6 -2
- mcp_agent/core/validation.py +89 -16
- mcp_agent/executor/decorator_registry.py +6 -2
- mcp_agent/executor/temporal.py +35 -11
- mcp_agent/executor/workflow_signal.py +8 -2
- mcp_agent/human_input/handler.py +3 -1
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/{workflows/llm → llm}/augmented_llm.py +131 -256
- mcp_agent/{workflows/llm → llm}/augmented_llm_passthrough.py +35 -107
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +26 -8
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +5 -1
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +37 -141
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +112 -148
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +78 -35
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +73 -44
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +18 -4
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +3 -3
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +3 -3
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +0 -21
- mcp_agent/{workflows/llm → llm}/sampling_format_converter.py +16 -1
- mcp_agent/logging/logger.py +2 -2
- mcp_agent/mcp/gen_client.py +9 -3
- mcp_agent/mcp/interfaces.py +67 -45
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +12 -4
- mcp_agent/mcp/mcp_agent_server.py +3 -1
- mcp_agent/mcp/mcp_aggregator.py +124 -93
- mcp_agent/mcp/mcp_connection_manager.py +21 -7
- mcp_agent/mcp/prompt_message_multipart.py +59 -1
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +20 -13
- mcp_agent/mcp/prompts/prompt_constants.py +18 -0
- mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
- mcp_agent/mcp/prompts/prompt_load.py +15 -5
- mcp_agent/mcp/prompts/prompt_server.py +154 -87
- mcp_agent/mcp/prompts/prompt_template.py +26 -35
- mcp_agent/mcp/resource_utils.py +3 -1
- mcp_agent/mcp/sampling.py +24 -15
- mcp_agent/mcp_server/agent_server.py +8 -5
- mcp_agent/mcp_server_registry.py +22 -9
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +1 -1
- mcp_agent/resources/examples/{data-analysis → in_dev}/slides.py +1 -1
- mcp_agent/resources/examples/internal/agent.py +4 -2
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/prompting/image_server.py +3 -1
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +27 -7
- fast_agent_mcp-0.1.13.dist-info/RECORD +0 -164
- mcp_agent/core/agent_app.py +0 -570
- mcp_agent/core/agent_utils.py +0 -69
- mcp_agent/core/decorators.py +0 -448
- mcp_agent/core/factory.py +0 -422
- mcp_agent/core/proxies.py +0 -278
- mcp_agent/core/types.py +0 -22
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -114
- mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
- mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
- mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
- mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
- mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
- mcp_agent/resources/examples/researcher/researcher-imp.py +0 -189
- mcp_agent/resources/examples/researcher/researcher.py +0 -39
- mcp_agent/resources/examples/workflows/chaining.py +0 -45
- mcp_agent/resources/examples/workflows/evaluator.py +0 -79
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -26
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -74
- mcp_agent/resources/examples/workflows/parallel.py +0 -79
- mcp_agent/resources/examples/workflows/router.py +0 -54
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -19
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -58
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -37
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -447
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -117
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -130
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -41
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -150
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -58
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -111
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -535
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -320
- mcp_agent/workflows/parallel/fan_out.py +0 -181
- mcp_agent/workflows/parallel/parallel_llm.py +0 -149
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -338
- mcp_agent/workflows/router/router_embedding.py +0 -226
- mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
- mcp_agent/workflows/router/router_embedding_openai.py +0 -59
- mcp_agent/workflows/router/router_llm.py +0 -304
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -292
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.13.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
- /mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_prompts.py +0 -0
- /mcp_agent/{workflows/llm → llm}/memory.py +0 -0
- /mcp_agent/{workflows/llm → llm}/prompt_utils.py +0 -0
@@ -0,0 +1,182 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Any, List, Optional
|
3
|
+
|
4
|
+
from mcp.types import TextContent
|
5
|
+
|
6
|
+
from mcp_agent.agents.agent import Agent, AgentConfig
|
7
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
8
|
+
from mcp_agent.core.request_params import RequestParams
|
9
|
+
from mcp_agent.mcp.interfaces import ModelT
|
10
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
11
|
+
|
12
|
+
|
13
|
+
class ParallelAgent(BaseAgent):
|
14
|
+
"""
|
15
|
+
LLMs can sometimes work simultaneously on a task (fan-out)
|
16
|
+
and have their outputs aggregated programmatically (fan-in).
|
17
|
+
This workflow performs both the fan-out and fan-in operations using LLMs.
|
18
|
+
From the user's perspective, an input is specified and the output is returned.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
config: AgentConfig,
|
24
|
+
fan_in_agent: Agent,
|
25
|
+
fan_out_agents: List[Agent],
|
26
|
+
include_request: bool = True,
|
27
|
+
**kwargs,
|
28
|
+
) -> None:
|
29
|
+
"""
|
30
|
+
Initialize a ParallelLLM agent.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
config: Agent configuration or name
|
34
|
+
fan_in_agent: Agent that aggregates results from fan-out agents
|
35
|
+
fan_out_agents: List of agents to execute in parallel
|
36
|
+
include_request: Whether to include the original request in the aggregation
|
37
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
38
|
+
"""
|
39
|
+
super().__init__(config, **kwargs)
|
40
|
+
self.fan_in_agent = fan_in_agent
|
41
|
+
self.fan_out_agents = fan_out_agents
|
42
|
+
self.include_request = include_request
|
43
|
+
|
44
|
+
async def generate(
|
45
|
+
self,
|
46
|
+
multipart_messages: List[PromptMessageMultipart],
|
47
|
+
request_params: Optional[RequestParams] = None,
|
48
|
+
) -> PromptMessageMultipart:
|
49
|
+
"""
|
50
|
+
Execute fan-out agents in parallel and aggregate their results with the fan-in agent.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
multipart_messages: List of messages to send to the fan-out agents
|
54
|
+
request_params: Optional parameters to configure the request
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
The aggregated response from the fan-in agent
|
58
|
+
"""
|
59
|
+
# Execute all fan-out agents in parallel
|
60
|
+
responses: List[PromptMessageMultipart] = await asyncio.gather(
|
61
|
+
*[agent.generate(multipart_messages, request_params) for agent in self.fan_out_agents]
|
62
|
+
)
|
63
|
+
|
64
|
+
# Extract the received message from the input
|
65
|
+
received_message: Optional[str] = (
|
66
|
+
multipart_messages[-1].all_text() if multipart_messages else None
|
67
|
+
)
|
68
|
+
|
69
|
+
# Convert responses to strings for aggregation
|
70
|
+
string_responses = []
|
71
|
+
for response in responses:
|
72
|
+
string_responses.append(response.all_text())
|
73
|
+
|
74
|
+
# Format the responses and send to the fan-in agent
|
75
|
+
aggregated_prompt = self._format_responses(string_responses, received_message)
|
76
|
+
|
77
|
+
# Create a new multipart message with the formatted responses
|
78
|
+
formatted_prompt = PromptMessageMultipart(
|
79
|
+
role="user", content=[TextContent(type="text", text=aggregated_prompt)]
|
80
|
+
)
|
81
|
+
|
82
|
+
# Use the fan-in agent to aggregate the responses
|
83
|
+
return await self.fan_in_agent.generate([formatted_prompt], request_params)
|
84
|
+
|
85
|
+
def _format_responses(self, responses: List[Any], message: Optional[str] = None) -> str:
|
86
|
+
"""
|
87
|
+
Format a list of responses for the fan-in agent.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
responses: List of responses from fan-out agents
|
91
|
+
message: Optional original message that was sent to the agents
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Formatted string with responses
|
95
|
+
"""
|
96
|
+
formatted = []
|
97
|
+
|
98
|
+
# Include the original message if specified
|
99
|
+
if self.include_request and message:
|
100
|
+
formatted.append("The following request was sent to the agents:")
|
101
|
+
formatted.append(f"<fastagent:request>\n{message}\n</fastagent:request>")
|
102
|
+
|
103
|
+
# Format each agent's response
|
104
|
+
for i, response in enumerate(responses):
|
105
|
+
agent_name = self.fan_out_agents[i].name
|
106
|
+
formatted.append(
|
107
|
+
f'<fastagent:response agent="{agent_name}">\n{response}\n</fastagent:response>'
|
108
|
+
)
|
109
|
+
return "\n\n".join(formatted)
|
110
|
+
|
111
|
+
async def structured(
|
112
|
+
self,
|
113
|
+
prompt: List[PromptMessageMultipart],
|
114
|
+
model: type[ModelT],
|
115
|
+
request_params: Optional[RequestParams] = None,
|
116
|
+
) -> Optional[ModelT]:
|
117
|
+
"""
|
118
|
+
Apply the prompt and return the result as a Pydantic model.
|
119
|
+
|
120
|
+
This implementation delegates to the fan-in agent's structured method.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
prompt: List of PromptMessageMultipart objects
|
124
|
+
model: The Pydantic model class to parse the result into
|
125
|
+
request_params: Optional parameters to configure the LLM request
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
An instance of the specified model, or None if coercion fails
|
129
|
+
"""
|
130
|
+
# Generate parallel responses first
|
131
|
+
responses: List[PromptMessageMultipart] = await asyncio.gather(
|
132
|
+
*[agent.generate(prompt, request_params) for agent in self.fan_out_agents]
|
133
|
+
)
|
134
|
+
|
135
|
+
# Extract the received message
|
136
|
+
received_message: Optional[str] = prompt[-1].all_text() if prompt else None
|
137
|
+
|
138
|
+
# Convert responses to strings
|
139
|
+
string_responses = [response.all_text() for response in responses]
|
140
|
+
|
141
|
+
# Format the responses for the fan-in agent
|
142
|
+
aggregated_prompt = self._format_responses(string_responses, received_message)
|
143
|
+
|
144
|
+
# Create a multipart message
|
145
|
+
formatted_prompt = PromptMessageMultipart(
|
146
|
+
role="user", content=[TextContent(type="text", text=aggregated_prompt)]
|
147
|
+
)
|
148
|
+
|
149
|
+
# Use the fan-in agent to parse the structured output
|
150
|
+
return await self.fan_in_agent.structured([formatted_prompt], model, request_params)
|
151
|
+
|
152
|
+
async def initialize(self) -> None:
|
153
|
+
"""
|
154
|
+
Initialize the agent and its fan-in and fan-out agents.
|
155
|
+
"""
|
156
|
+
await super().initialize()
|
157
|
+
|
158
|
+
# Initialize fan-in and fan-out agents if not already initialized
|
159
|
+
if not getattr(self.fan_in_agent, "initialized", False):
|
160
|
+
await self.fan_in_agent.initialize()
|
161
|
+
|
162
|
+
for agent in self.fan_out_agents:
|
163
|
+
if not getattr(agent, "initialized", False):
|
164
|
+
await agent.initialize()
|
165
|
+
|
166
|
+
async def shutdown(self) -> None:
|
167
|
+
"""
|
168
|
+
Shutdown the agent and its fan-in and fan-out agents.
|
169
|
+
"""
|
170
|
+
await super().shutdown()
|
171
|
+
|
172
|
+
# Shutdown fan-in and fan-out agents
|
173
|
+
try:
|
174
|
+
await self.fan_in_agent.shutdown()
|
175
|
+
except Exception as e:
|
176
|
+
self.logger.warning(f"Error shutting down fan-in agent: {str(e)}")
|
177
|
+
|
178
|
+
for agent in self.fan_out_agents:
|
179
|
+
try:
|
180
|
+
await agent.shutdown()
|
181
|
+
except Exception as e:
|
182
|
+
self.logger.warning(f"Error shutting down fan-out agent {agent.name}: {str(e)}")
|
@@ -0,0 +1,307 @@
|
|
1
|
+
"""
|
2
|
+
Router agent implementation using the BaseAgent adapter pattern.
|
3
|
+
|
4
|
+
This provides a simplified implementation that routes messages to agents
|
5
|
+
by determining the best agent for a request and dispatching to it.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import TYPE_CHECKING, List, Optional, Type
|
9
|
+
|
10
|
+
from mcp.types import TextContent
|
11
|
+
from pydantic import BaseModel
|
12
|
+
|
13
|
+
from mcp_agent.agents.agent import Agent
|
14
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
15
|
+
from mcp_agent.core.agent_types import AgentConfig
|
16
|
+
from mcp_agent.core.exceptions import AgentConfigError
|
17
|
+
from mcp_agent.core.request_params import RequestParams
|
18
|
+
from mcp_agent.logging.logger import get_logger
|
19
|
+
from mcp_agent.mcp.interfaces import ModelT
|
20
|
+
from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
|
21
|
+
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from mcp_agent.context import Context
|
24
|
+
|
25
|
+
logger = get_logger(__name__)
|
26
|
+
|
27
|
+
# Simple system instruction for the router
|
28
|
+
ROUTING_SYSTEM_INSTRUCTION = """
|
29
|
+
You are a highly accurate request router that directs incoming requests to the most appropriate agent.
|
30
|
+
Analyze each request and determine which specialized agent would be best suited to handle it based on their capabilities.
|
31
|
+
|
32
|
+
Follow these guidelines:
|
33
|
+
- Carefully match the request's needs with each agent's capabilities and description
|
34
|
+
- Select the single most appropriate agent for the request
|
35
|
+
- Provide your confidence level (high, medium, low) and brief reasoning for your selection
|
36
|
+
"""
|
37
|
+
|
38
|
+
# Default routing instruction with placeholders for context and request
|
39
|
+
DEFAULT_ROUTING_INSTRUCTION = """
|
40
|
+
You are a highly accurate request router that directs incoming requests to the most appropriate agent.
|
41
|
+
|
42
|
+
<fastagent:data>
|
43
|
+
<fastagent:agents>
|
44
|
+
{context}
|
45
|
+
</fastagent:agents>
|
46
|
+
|
47
|
+
<fastagent:request>
|
48
|
+
{request}
|
49
|
+
</fastagent:request>
|
50
|
+
</fastagent:data>
|
51
|
+
|
52
|
+
Your task is to analyze the request and determine the most appropriate agent from the options above.
|
53
|
+
|
54
|
+
<fastagent:instruction>
|
55
|
+
Respond in JSON format. NEVER include Code Fences:
|
56
|
+
{{
|
57
|
+
"agent": "<agent name>",
|
58
|
+
"confidence": "<high, medium or low>",
|
59
|
+
"reasoning": "<brief explanation>"
|
60
|
+
}}
|
61
|
+
</fastagent:instruction>
|
62
|
+
"""
|
63
|
+
|
64
|
+
|
65
|
+
class RoutingResponse(BaseModel):
|
66
|
+
"""Model for the structured routing response from the LLM."""
|
67
|
+
|
68
|
+
agent: str
|
69
|
+
confidence: str
|
70
|
+
reasoning: Optional[str] = None
|
71
|
+
|
72
|
+
|
73
|
+
class RouterResult(BaseModel):
|
74
|
+
"""Router result with agent reference and confidence rating."""
|
75
|
+
|
76
|
+
result: Agent
|
77
|
+
confidence: str
|
78
|
+
reasoning: Optional[str] = None
|
79
|
+
|
80
|
+
# Allow Agent objects to be stored without serialization
|
81
|
+
model_config = {"arbitrary_types_allowed": True}
|
82
|
+
|
83
|
+
|
84
|
+
class RouterAgent(BaseAgent):
|
85
|
+
"""
|
86
|
+
A simplified router that uses an LLM to determine the best agent for a request,
|
87
|
+
then dispatches the request to that agent and returns the response.
|
88
|
+
"""
|
89
|
+
|
90
|
+
def __init__(
|
91
|
+
self,
|
92
|
+
config: AgentConfig,
|
93
|
+
agents: List[Agent],
|
94
|
+
routing_instruction: Optional[str] = None,
|
95
|
+
context: Optional["Context"] = None,
|
96
|
+
default_request_params: Optional[RequestParams] = None,
|
97
|
+
**kwargs,
|
98
|
+
) -> None:
|
99
|
+
"""
|
100
|
+
Initialize a RouterAgent.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
config: Agent configuration or name
|
104
|
+
agents: List of agents to route between
|
105
|
+
routing_instruction: Optional custom routing instruction
|
106
|
+
context: Optional application context
|
107
|
+
default_request_params: Optional default request parameters
|
108
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
109
|
+
"""
|
110
|
+
super().__init__(config=config, context=context, **kwargs)
|
111
|
+
|
112
|
+
if not agents:
|
113
|
+
raise AgentConfigError("At least one agent must be provided")
|
114
|
+
|
115
|
+
self.agents = agents
|
116
|
+
self.routing_instruction = routing_instruction
|
117
|
+
self.agent_map = {agent.name: agent for agent in agents}
|
118
|
+
|
119
|
+
# Set up base router request parameters
|
120
|
+
base_params = {"systemPrompt": ROUTING_SYSTEM_INSTRUCTION, "use_history": False}
|
121
|
+
|
122
|
+
# Merge with provided defaults if any
|
123
|
+
if default_request_params:
|
124
|
+
# Start with defaults and override with router-specific settings
|
125
|
+
merged_params = default_request_params.model_copy(update=base_params)
|
126
|
+
else:
|
127
|
+
merged_params = RequestParams(**base_params)
|
128
|
+
|
129
|
+
self._default_request_params = merged_params
|
130
|
+
|
131
|
+
async def initialize(self) -> None:
|
132
|
+
"""Initialize the router and all agents."""
|
133
|
+
if not self.initialized:
|
134
|
+
await super().initialize()
|
135
|
+
|
136
|
+
# Initialize all agents if not already initialized
|
137
|
+
for agent in self.agents:
|
138
|
+
if not getattr(agent, "initialized", False):
|
139
|
+
await agent.initialize()
|
140
|
+
|
141
|
+
self.initialized = True
|
142
|
+
|
143
|
+
async def shutdown(self) -> None:
|
144
|
+
"""Shutdown the router and all agents."""
|
145
|
+
await super().shutdown()
|
146
|
+
|
147
|
+
# Shutdown all agents
|
148
|
+
for agent in self.agents:
|
149
|
+
try:
|
150
|
+
await agent.shutdown()
|
151
|
+
except Exception as e:
|
152
|
+
logger.warning(f"Error shutting down agent: {str(e)}")
|
153
|
+
|
154
|
+
async def _get_routing_result(
|
155
|
+
self,
|
156
|
+
messages: List[PromptMessageMultipart],
|
157
|
+
) -> Optional[RouterResult]:
|
158
|
+
"""
|
159
|
+
Common method to extract request and get routing result.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
messages: The messages to extract request from
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
RouterResult containing the selected agent, or None if no suitable agent found
|
166
|
+
"""
|
167
|
+
if not self.initialized:
|
168
|
+
await self.initialize()
|
169
|
+
|
170
|
+
# Extract the request text from the last message
|
171
|
+
request = messages[-1].all_text() if messages else ""
|
172
|
+
|
173
|
+
# Determine which agent to route to
|
174
|
+
routing_result = await self._route_request(request)
|
175
|
+
|
176
|
+
if not routing_result:
|
177
|
+
logger.warning("Could not determine appropriate agent for this request")
|
178
|
+
|
179
|
+
return routing_result
|
180
|
+
|
181
|
+
async def generate(
|
182
|
+
self,
|
183
|
+
multipart_messages: List[PromptMessageMultipart],
|
184
|
+
request_params: Optional[RequestParams] = None,
|
185
|
+
) -> PromptMessageMultipart:
|
186
|
+
"""
|
187
|
+
Route the request to the most appropriate agent and return its response.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
multipart_messages: Messages to route
|
191
|
+
request_params: Optional request parameters
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
The response from the selected agent
|
195
|
+
"""
|
196
|
+
routing_result = await self._get_routing_result(multipart_messages)
|
197
|
+
|
198
|
+
if not routing_result:
|
199
|
+
return PromptMessageMultipart(
|
200
|
+
role="assistant",
|
201
|
+
content=[
|
202
|
+
TextContent(
|
203
|
+
type="text", text="Could not determine appropriate agent for this request."
|
204
|
+
)
|
205
|
+
],
|
206
|
+
)
|
207
|
+
|
208
|
+
# Get the selected agent
|
209
|
+
selected_agent = routing_result.result
|
210
|
+
|
211
|
+
# Log the routing decision
|
212
|
+
logger.info(
|
213
|
+
f"Routing request to agent: {selected_agent.name} (confidence: {routing_result.confidence})"
|
214
|
+
)
|
215
|
+
|
216
|
+
# Dispatch the request to the selected agent
|
217
|
+
return await selected_agent.generate(multipart_messages, request_params)
|
218
|
+
|
219
|
+
async def structured(
|
220
|
+
self,
|
221
|
+
prompt: List[PromptMessageMultipart],
|
222
|
+
model: Type[ModelT],
|
223
|
+
request_params: Optional[RequestParams] = None,
|
224
|
+
) -> Optional[ModelT]:
|
225
|
+
"""
|
226
|
+
Route the request to the most appropriate agent and parse its response.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
prompt: Messages to route
|
230
|
+
model: Pydantic model to parse the response into
|
231
|
+
request_params: Optional request parameters
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
The parsed response from the selected agent, or None if parsing fails
|
235
|
+
"""
|
236
|
+
routing_result = await self._get_routing_result(prompt)
|
237
|
+
|
238
|
+
if not routing_result:
|
239
|
+
return None
|
240
|
+
|
241
|
+
# Get the selected agent
|
242
|
+
selected_agent = routing_result.result
|
243
|
+
|
244
|
+
# Log the routing decision
|
245
|
+
logger.info(
|
246
|
+
f"Routing structured request to agent: {selected_agent.name} (confidence: {routing_result.confidence})"
|
247
|
+
)
|
248
|
+
|
249
|
+
# Dispatch the request to the selected agent
|
250
|
+
return await selected_agent.structured(prompt, model, request_params)
|
251
|
+
|
252
|
+
async def _route_request(self, request: str) -> Optional[RouterResult]:
|
253
|
+
"""
|
254
|
+
Determine which agent to route the request to.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
request: The request to route
|
258
|
+
|
259
|
+
Returns:
|
260
|
+
RouterResult containing the selected agent, or None if no suitable agent was found
|
261
|
+
"""
|
262
|
+
if not self.agents:
|
263
|
+
logger.warning("No agents available for routing")
|
264
|
+
return None
|
265
|
+
|
266
|
+
# If only one agent is available, use it directly
|
267
|
+
if len(self.agents) == 1:
|
268
|
+
return RouterResult(
|
269
|
+
result=self.agents[0], confidence="high", reasoning="Only one agent available"
|
270
|
+
)
|
271
|
+
|
272
|
+
# Generate agent descriptions for the context
|
273
|
+
agent_descriptions = []
|
274
|
+
for i, agent in enumerate(self.agents, 1):
|
275
|
+
description = agent.instruction if isinstance(agent.instruction, str) else ""
|
276
|
+
agent_descriptions.append(f"{i}. Name: {agent.name} - {description}")
|
277
|
+
|
278
|
+
context = "\n\n".join(agent_descriptions)
|
279
|
+
|
280
|
+
# Format the routing prompt
|
281
|
+
routing_instruction = self.routing_instruction or DEFAULT_ROUTING_INSTRUCTION
|
282
|
+
prompt_text = routing_instruction.format(context=context, request=request)
|
283
|
+
|
284
|
+
# Create multipart message for the router
|
285
|
+
prompt = PromptMessageMultipart(
|
286
|
+
role="user", content=[TextContent(type="text", text=prompt_text)]
|
287
|
+
)
|
288
|
+
|
289
|
+
# Get structured response from LLM
|
290
|
+
response = await self._llm.structured(
|
291
|
+
[prompt], RoutingResponse, self._default_request_params
|
292
|
+
)
|
293
|
+
|
294
|
+
if not response:
|
295
|
+
logger.warning("No routing response received from LLM")
|
296
|
+
return None
|
297
|
+
|
298
|
+
# Look up the agent by name
|
299
|
+
selected_agent = self.agent_map.get(response.agent)
|
300
|
+
|
301
|
+
if not selected_agent:
|
302
|
+
logger.warning(f"Agent '{response.agent}' not found in available agents")
|
303
|
+
return None
|
304
|
+
|
305
|
+
return RouterResult(
|
306
|
+
result=selected_agent, confidence=response.confidence, reasoning=response.reasoning
|
307
|
+
)
|
mcp_agent/app.py
CHANGED
@@ -72,7 +72,9 @@ class MCPApp:
|
|
72
72
|
@property
|
73
73
|
def context(self) -> Context:
|
74
74
|
if self._context is None:
|
75
|
-
raise RuntimeError(
|
75
|
+
raise RuntimeError(
|
76
|
+
"MCPApp not initialized, please call initialize() first, or use async with app.run()."
|
77
|
+
)
|
76
78
|
return self._context
|
77
79
|
|
78
80
|
@property
|
@@ -70,8 +70,9 @@ def copy_example_files(example_type: str, target_dir: Path, force: bool = False)
|
|
70
70
|
mount_point_dir.mkdir(parents=True)
|
71
71
|
console.print(f"Created mount-point directory: {mount_point_dir}")
|
72
72
|
|
73
|
-
# Use
|
74
|
-
|
73
|
+
# Use examples from top-level directory
|
74
|
+
package_dir = Path(__file__).parent.parent.parent.parent.parent
|
75
|
+
source_dir = package_dir / "examples" / ("workflows" if example_type == "workflow" else f"{example_type}")
|
75
76
|
|
76
77
|
if not source_dir.exists():
|
77
78
|
console.print(f"[red]Error: Source directory not found: {source_dir}[/red]")
|
@@ -110,7 +111,9 @@ def copy_example_files(example_type: str, target_dir: Path, force: bool = False)
|
|
110
111
|
continue
|
111
112
|
|
112
113
|
if target.exists() and not force:
|
113
|
-
console.print(
|
114
|
+
console.print(
|
115
|
+
f"[yellow]Skipping[/yellow] mount-point/{filename} (already exists)"
|
116
|
+
)
|
114
117
|
continue
|
115
118
|
|
116
119
|
shutil.copy2(source, target)
|
@@ -137,7 +140,9 @@ def show_overview() -> None:
|
|
137
140
|
for name, info in EXAMPLE_TYPES.items():
|
138
141
|
files_list = "\n".join(f"• {f}" for f in info["files"])
|
139
142
|
if "mount_point_files" in info:
|
140
|
-
files_list += "\n[blue]mount-point:[/blue]\n" + "\n".join(
|
143
|
+
files_list += "\n[blue]mount-point:[/blue]\n" + "\n".join(
|
144
|
+
f"• {f}" for f in info["mount_point_files"]
|
145
|
+
)
|
141
146
|
table.add_row(f"[green]{name}[/green]", info["description"], files_list)
|
142
147
|
|
143
148
|
console.print(table)
|
@@ -229,17 +234,23 @@ def _show_completion_message(example_type: str, created: list[str]) -> None:
|
|
229
234
|
console.print(" - evaluator.py: Add evaluation capabilities")
|
230
235
|
console.print(" - human_input.py: Incorporate human feedback")
|
231
236
|
console.print("3. Run an example with: uv run <example>.py")
|
232
|
-
console.print(
|
237
|
+
console.print(
|
238
|
+
"4. Try a different model with --model=<model>, or update the agent config"
|
239
|
+
)
|
233
240
|
|
234
241
|
elif example_type == "researcher":
|
235
|
-
console.print(
|
242
|
+
console.print(
|
243
|
+
"1. Set up the Brave MCP Server (get an API key from https://brave.com/search/api/)"
|
244
|
+
)
|
236
245
|
console.print("2. Try `uv run researcher.py` for the basic version")
|
237
246
|
console.print("3. Try `uv run researcher-eval.py` for the eval/optimize version")
|
238
247
|
elif example_type == "data-analysis":
|
239
248
|
console.print("1. Run uv `analysis.py` to perform data analysis and visualization")
|
240
249
|
console.print("2. The dataset is available in the mount-point directory:")
|
241
250
|
console.print(" - mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv")
|
242
|
-
console.print(
|
251
|
+
console.print(
|
252
|
+
"On Windows platforms, please edit the fastagent.config.yaml and adjust the volume mount point."
|
253
|
+
)
|
243
254
|
else:
|
244
255
|
console.print("\n[yellow]No files were created.[/yellow]")
|
245
256
|
|
mcp_agent/cli/commands/setup.py
CHANGED
@@ -173,7 +173,9 @@ def init(
|
|
173
173
|
|
174
174
|
config_path = Path(config_dir).resolve()
|
175
175
|
if not config_path.exists():
|
176
|
-
should_create = Confirm.ask(
|
176
|
+
should_create = Confirm.ask(
|
177
|
+
f"Directory {config_path} does not exist. Create it?", default=True
|
178
|
+
)
|
177
179
|
if should_create:
|
178
180
|
config_path.mkdir(parents=True)
|
179
181
|
else:
|
@@ -212,9 +214,15 @@ def init(
|
|
212
214
|
console.print("\n[green]Setup completed successfully![/green]")
|
213
215
|
if "fastagent.secrets.yaml" in created:
|
214
216
|
console.print("\n[yellow]Important:[/yellow] Remember to:")
|
215
|
-
console.print(
|
216
|
-
|
217
|
-
|
217
|
+
console.print(
|
218
|
+
"1. Add your API keys to fastagent-secrets.yaml or set OPENAI_API_KEY and ANTHROPIC_API_KEY environment variables"
|
219
|
+
)
|
220
|
+
console.print(
|
221
|
+
"2. Keep fastagent.secrets.yaml secure and never commit it to version control"
|
222
|
+
)
|
223
|
+
console.print(
|
224
|
+
"3. Update fastagent.config.yaml to set a default model (currently system default is 'haiku')"
|
225
|
+
)
|
218
226
|
console.print("\nTo get started, run:")
|
219
227
|
console.print(" uv run agent.py")
|
220
228
|
else:
|
mcp_agent/cli/main.py
CHANGED
@@ -8,7 +8,7 @@ from mcp_agent.cli.commands import bootstrap, setup
|
|
8
8
|
from mcp_agent.cli.terminal import Application
|
9
9
|
|
10
10
|
app = typer.Typer(
|
11
|
-
help="
|
11
|
+
help="FastAgent CLI - Build effective agents using Model Context Protocol",
|
12
12
|
add_completion=False, # We'll add this later when we have more commands
|
13
13
|
)
|
14
14
|
|
mcp_agent/cli/terminal.py
CHANGED
@@ -14,7 +14,7 @@ class Application:
|
|
14
14
|
self.error_console = error_console
|
15
15
|
|
16
16
|
def log(self, message: str, level: str = "info") -> None:
|
17
|
-
if level == "info" or (level == "debug" and self.verbosity > 0):
|
17
|
+
if (level == "info" or (level == "debug" and self.verbosity > 0) or level == "error"):
|
18
18
|
if level == "error":
|
19
19
|
self.error_console.print(f"[{level.upper()}] {message}")
|
20
20
|
else:
|