fast-agent-mcp 0.1.12__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.12.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 +61 -415
- 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 +11 -21
- mcp_agent/agents/workflow/parallel_agent.py +182 -0
- mcp_agent/agents/workflow/router_agent.py +307 -0
- mcp_agent/app.py +15 -19
- mcp_agent/cli/commands/bootstrap.py +19 -38
- mcp_agent/cli/commands/config.py +4 -4
- mcp_agent/cli/commands/setup.py +7 -14
- mcp_agent/cli/main.py +7 -10
- mcp_agent/cli/terminal.py +3 -3
- mcp_agent/config.py +25 -40
- mcp_agent/context.py +12 -21
- mcp_agent/context_dependent.py +3 -5
- 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 +23 -55
- mcp_agent/core/exceptions.py +8 -8
- mcp_agent/core/fastagent.py +145 -371
- mcp_agent/core/interactive_prompt.py +424 -0
- mcp_agent/core/mcp_content.py +17 -17
- mcp_agent/core/prompt.py +6 -9
- mcp_agent/core/request_params.py +6 -3
- mcp_agent/core/validation.py +92 -18
- mcp_agent/executor/decorator_registry.py +9 -17
- mcp_agent/executor/executor.py +8 -17
- mcp_agent/executor/task_registry.py +2 -4
- mcp_agent/executor/temporal.py +19 -41
- mcp_agent/executor/workflow.py +3 -5
- mcp_agent/executor/workflow_signal.py +15 -21
- mcp_agent/human_input/handler.py +4 -7
- mcp_agent/human_input/types.py +2 -3
- mcp_agent/llm/__init__.py +2 -0
- mcp_agent/llm/augmented_llm.py +450 -0
- mcp_agent/llm/augmented_llm_passthrough.py +162 -0
- mcp_agent/llm/augmented_llm_playback.py +83 -0
- mcp_agent/llm/memory.py +103 -0
- mcp_agent/{workflows/llm → llm}/model_factory.py +22 -16
- mcp_agent/{workflows/llm → llm}/prompt_utils.py +1 -3
- mcp_agent/llm/providers/__init__.py +8 -0
- mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +8 -25
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +56 -194
- mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
- mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +99 -190
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +72 -71
- mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +65 -71
- mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +16 -44
- mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +4 -4
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +9 -11
- mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +8 -12
- mcp_agent/{workflows/llm → llm}/sampling_converter.py +3 -31
- mcp_agent/llm/sampling_format_converter.py +37 -0
- mcp_agent/logging/events.py +1 -5
- mcp_agent/logging/json_serializer.py +7 -6
- mcp_agent/logging/listeners.py +20 -23
- mcp_agent/logging/logger.py +17 -19
- mcp_agent/logging/rich_progress.py +10 -8
- mcp_agent/logging/tracing.py +4 -6
- mcp_agent/logging/transport.py +22 -22
- mcp_agent/mcp/gen_client.py +1 -3
- mcp_agent/mcp/interfaces.py +117 -110
- mcp_agent/mcp/logger_textio.py +97 -0
- mcp_agent/mcp/mcp_agent_client_session.py +7 -7
- mcp_agent/mcp/mcp_agent_server.py +8 -8
- mcp_agent/mcp/mcp_aggregator.py +102 -143
- mcp_agent/mcp/mcp_connection_manager.py +20 -27
- mcp_agent/mcp/prompt_message_multipart.py +68 -16
- mcp_agent/mcp/prompt_render.py +77 -0
- mcp_agent/mcp/prompt_serialization.py +30 -48
- 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 +109 -0
- mcp_agent/mcp/prompts/prompt_server.py +155 -195
- mcp_agent/mcp/prompts/prompt_template.py +35 -66
- mcp_agent/mcp/resource_utils.py +7 -14
- mcp_agent/mcp/sampling.py +17 -17
- mcp_agent/mcp_server/agent_server.py +13 -17
- mcp_agent/mcp_server_registry.py +13 -22
- mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +3 -2
- mcp_agent/resources/examples/in_dev/slides.py +110 -0
- mcp_agent/resources/examples/internal/agent.py +6 -3
- mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
- mcp_agent/resources/examples/internal/job.py +2 -1
- mcp_agent/resources/examples/internal/prompt_category.py +1 -1
- mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
- mcp_agent/resources/examples/internal/sizer.py +2 -1
- mcp_agent/resources/examples/internal/social.py +2 -1
- mcp_agent/resources/examples/prompting/agent.py +2 -1
- mcp_agent/resources/examples/prompting/image_server.py +4 -8
- mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
- mcp_agent/ui/console_display.py +16 -20
- fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
- mcp_agent/core/agent_app.py +0 -646
- mcp_agent/core/agent_utils.py +0 -71
- mcp_agent/core/decorators.py +0 -455
- mcp_agent/core/factory.py +0 -463
- mcp_agent/core/proxies.py +0 -269
- mcp_agent/core/types.py +0 -24
- mcp_agent/eval/__init__.py +0 -0
- mcp_agent/mcp/stdio.py +0 -111
- 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 -190
- mcp_agent/resources/examples/researcher/researcher.py +0 -38
- mcp_agent/resources/examples/workflows/chaining.py +0 -44
- mcp_agent/resources/examples/workflows/evaluator.py +0 -78
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
- mcp_agent/resources/examples/workflows/human_input.py +0 -25
- mcp_agent/resources/examples/workflows/orchestrator.py +0 -73
- mcp_agent/resources/examples/workflows/parallel.py +0 -78
- mcp_agent/resources/examples/workflows/router.py +0 -53
- mcp_agent/resources/examples/workflows/sse.py +0 -23
- mcp_agent/telemetry/__init__.py +0 -0
- mcp_agent/telemetry/usage_tracking.py +0 -18
- mcp_agent/workflows/__init__.py +0 -0
- mcp_agent/workflows/embedding/__init__.py +0 -0
- mcp_agent/workflows/embedding/embedding_base.py +0 -61
- mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
- mcp_agent/workflows/embedding/embedding_openai.py +0 -46
- mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
- mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -481
- mcp_agent/workflows/intent_classifier/__init__.py +0 -0
- mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -120
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -134
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -45
- mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -161
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
- mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -60
- mcp_agent/workflows/llm/__init__.py +0 -0
- mcp_agent/workflows/llm/augmented_llm.py +0 -753
- mcp_agent/workflows/llm/augmented_llm_passthrough.py +0 -241
- mcp_agent/workflows/llm/augmented_llm_playback.py +0 -109
- mcp_agent/workflows/llm/providers/__init__.py +0 -8
- mcp_agent/workflows/llm/sampling_format_converter.py +0 -22
- mcp_agent/workflows/orchestrator/__init__.py +0 -0
- mcp_agent/workflows/orchestrator/orchestrator.py +0 -578
- mcp_agent/workflows/parallel/__init__.py +0 -0
- mcp_agent/workflows/parallel/fan_in.py +0 -350
- mcp_agent/workflows/parallel/fan_out.py +0 -187
- mcp_agent/workflows/parallel/parallel_llm.py +0 -166
- mcp_agent/workflows/router/__init__.py +0 -0
- mcp_agent/workflows/router/router_base.py +0 -368
- mcp_agent/workflows/router/router_embedding.py +0 -240
- 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 -320
- mcp_agent/workflows/swarm/__init__.py +0 -0
- mcp_agent/workflows/swarm/swarm.py +0 -320
- mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
- mcp_agent/workflows/swarm/swarm_openai.py +0 -41
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.1.12.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
@@ -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
@@ -1,16 +1,18 @@
|
|
1
|
-
from typing import Any, Dict, Optional, Type, TypeVar, Callable
|
2
|
-
from datetime import timedelta
|
3
1
|
import asyncio
|
4
2
|
from contextlib import asynccontextmanager
|
3
|
+
from datetime import timedelta
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, TypeVar
|
5
5
|
|
6
|
-
from mcp import ServerSession
|
7
|
-
from mcp_agent.context import Context, initialize_context, cleanup_context
|
8
6
|
from mcp_agent.config import Settings
|
7
|
+
from mcp_agent.context import Context, cleanup_context, initialize_context
|
9
8
|
from mcp_agent.event_progress import ProgressAction
|
10
|
-
from mcp_agent.logging.logger import get_logger
|
11
9
|
from mcp_agent.executor.workflow_signal import SignalWaitCallback
|
12
|
-
from mcp_agent.human_input.types import HumanInputCallback
|
13
10
|
from mcp_agent.human_input.handler import console_input_callback
|
11
|
+
from mcp_agent.human_input.types import HumanInputCallback
|
12
|
+
from mcp_agent.logging.logger import get_logger
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from mcp import ServerSession
|
14
16
|
|
15
17
|
R = TypeVar("R")
|
16
18
|
|
@@ -43,7 +45,7 @@ class MCPApp:
|
|
43
45
|
human_input_callback: Optional[HumanInputCallback] = console_input_callback,
|
44
46
|
signal_notification: Optional[SignalWaitCallback] = None,
|
45
47
|
upstream_session: Optional["ServerSession"] = None,
|
46
|
-
):
|
48
|
+
) -> None:
|
47
49
|
"""
|
48
50
|
Initialize the application with a name and optional settings.
|
49
51
|
Args:
|
@@ -96,7 +98,7 @@ class MCPApp:
|
|
96
98
|
return self._context.upstream_session
|
97
99
|
|
98
100
|
@upstream_session.setter
|
99
|
-
def upstream_session(self, value):
|
101
|
+
def upstream_session(self, value) -> None:
|
100
102
|
self._context.upstream_session = value
|
101
103
|
|
102
104
|
@property
|
@@ -113,7 +115,7 @@ class MCPApp:
|
|
113
115
|
self._logger = get_logger(f"mcp_agent.{self.name}")
|
114
116
|
return self._logger
|
115
117
|
|
116
|
-
async def initialize(self):
|
118
|
+
async def initialize(self) -> None:
|
117
119
|
"""Initialize the application."""
|
118
120
|
if self._initialized:
|
119
121
|
return
|
@@ -135,7 +137,7 @@ class MCPApp:
|
|
135
137
|
},
|
136
138
|
)
|
137
139
|
|
138
|
-
async def cleanup(self):
|
140
|
+
async def cleanup(self) -> None:
|
139
141
|
"""Cleanup application resources."""
|
140
142
|
if not self._initialized:
|
141
143
|
return
|
@@ -173,9 +175,7 @@ class MCPApp:
|
|
173
175
|
finally:
|
174
176
|
await self.cleanup()
|
175
177
|
|
176
|
-
def workflow(
|
177
|
-
self, cls: Type, *args, workflow_id: str | None = None, **kwargs
|
178
|
-
) -> Type:
|
178
|
+
def workflow(self, cls: Type, *args, workflow_id: str | None = None, **kwargs) -> Type:
|
179
179
|
"""
|
180
180
|
Decorator for a workflow class. By default it's a no-op,
|
181
181
|
but different executors can use this to customize behavior
|
@@ -187,9 +187,7 @@ class MCPApp:
|
|
187
187
|
"""
|
188
188
|
decorator_registry = self.context.decorator_registry
|
189
189
|
execution_engine = self.engine
|
190
|
-
workflow_defn_decorator = decorator_registry.get_workflow_defn_decorator(
|
191
|
-
execution_engine
|
192
|
-
)
|
190
|
+
workflow_defn_decorator = decorator_registry.get_workflow_defn_decorator(execution_engine)
|
193
191
|
|
194
192
|
if workflow_defn_decorator:
|
195
193
|
return workflow_defn_decorator(cls, *args, **kwargs)
|
@@ -211,9 +209,7 @@ class MCPApp:
|
|
211
209
|
|
212
210
|
decorator_registry = self.context.decorator_registry
|
213
211
|
execution_engine = self.engine
|
214
|
-
workflow_run_decorator = decorator_registry.get_workflow_run_decorator(
|
215
|
-
execution_engine
|
216
|
-
)
|
212
|
+
workflow_run_decorator = decorator_registry.get_workflow_run_decorator(execution_engine)
|
217
213
|
|
218
214
|
if workflow_run_decorator:
|
219
215
|
return workflow_run_decorator(fn)
|