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.
Files changed (169) hide show
  1. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/METADATA +3 -4
  2. fast_agent_mcp-0.2.0.dist-info/RECORD +123 -0
  3. mcp_agent/__init__.py +75 -0
  4. mcp_agent/agents/agent.py +61 -415
  5. mcp_agent/agents/base_agent.py +522 -0
  6. mcp_agent/agents/workflow/__init__.py +1 -0
  7. mcp_agent/agents/workflow/chain_agent.py +173 -0
  8. mcp_agent/agents/workflow/evaluator_optimizer.py +362 -0
  9. mcp_agent/agents/workflow/orchestrator_agent.py +591 -0
  10. mcp_agent/{workflows/orchestrator → agents/workflow}/orchestrator_models.py +11 -21
  11. mcp_agent/agents/workflow/parallel_agent.py +182 -0
  12. mcp_agent/agents/workflow/router_agent.py +307 -0
  13. mcp_agent/app.py +15 -19
  14. mcp_agent/cli/commands/bootstrap.py +19 -38
  15. mcp_agent/cli/commands/config.py +4 -4
  16. mcp_agent/cli/commands/setup.py +7 -14
  17. mcp_agent/cli/main.py +7 -10
  18. mcp_agent/cli/terminal.py +3 -3
  19. mcp_agent/config.py +25 -40
  20. mcp_agent/context.py +12 -21
  21. mcp_agent/context_dependent.py +3 -5
  22. mcp_agent/core/agent_types.py +10 -7
  23. mcp_agent/core/direct_agent_app.py +179 -0
  24. mcp_agent/core/direct_decorators.py +443 -0
  25. mcp_agent/core/direct_factory.py +476 -0
  26. mcp_agent/core/enhanced_prompt.py +23 -55
  27. mcp_agent/core/exceptions.py +8 -8
  28. mcp_agent/core/fastagent.py +145 -371
  29. mcp_agent/core/interactive_prompt.py +424 -0
  30. mcp_agent/core/mcp_content.py +17 -17
  31. mcp_agent/core/prompt.py +6 -9
  32. mcp_agent/core/request_params.py +6 -3
  33. mcp_agent/core/validation.py +92 -18
  34. mcp_agent/executor/decorator_registry.py +9 -17
  35. mcp_agent/executor/executor.py +8 -17
  36. mcp_agent/executor/task_registry.py +2 -4
  37. mcp_agent/executor/temporal.py +19 -41
  38. mcp_agent/executor/workflow.py +3 -5
  39. mcp_agent/executor/workflow_signal.py +15 -21
  40. mcp_agent/human_input/handler.py +4 -7
  41. mcp_agent/human_input/types.py +2 -3
  42. mcp_agent/llm/__init__.py +2 -0
  43. mcp_agent/llm/augmented_llm.py +450 -0
  44. mcp_agent/llm/augmented_llm_passthrough.py +162 -0
  45. mcp_agent/llm/augmented_llm_playback.py +83 -0
  46. mcp_agent/llm/memory.py +103 -0
  47. mcp_agent/{workflows/llm → llm}/model_factory.py +22 -16
  48. mcp_agent/{workflows/llm → llm}/prompt_utils.py +1 -3
  49. mcp_agent/llm/providers/__init__.py +8 -0
  50. mcp_agent/{workflows/llm → llm/providers}/anthropic_utils.py +8 -25
  51. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_anthropic.py +56 -194
  52. mcp_agent/llm/providers/augmented_llm_deepseek.py +53 -0
  53. mcp_agent/{workflows/llm → llm/providers}/augmented_llm_openai.py +99 -190
  54. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_anthropic.py +72 -71
  55. mcp_agent/{workflows/llm → llm}/providers/multipart_converter_openai.py +65 -71
  56. mcp_agent/{workflows/llm → llm}/providers/openai_multipart.py +16 -44
  57. mcp_agent/{workflows/llm → llm/providers}/openai_utils.py +4 -4
  58. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_anthropic.py +9 -11
  59. mcp_agent/{workflows/llm → llm}/providers/sampling_converter_openai.py +8 -12
  60. mcp_agent/{workflows/llm → llm}/sampling_converter.py +3 -31
  61. mcp_agent/llm/sampling_format_converter.py +37 -0
  62. mcp_agent/logging/events.py +1 -5
  63. mcp_agent/logging/json_serializer.py +7 -6
  64. mcp_agent/logging/listeners.py +20 -23
  65. mcp_agent/logging/logger.py +17 -19
  66. mcp_agent/logging/rich_progress.py +10 -8
  67. mcp_agent/logging/tracing.py +4 -6
  68. mcp_agent/logging/transport.py +22 -22
  69. mcp_agent/mcp/gen_client.py +1 -3
  70. mcp_agent/mcp/interfaces.py +117 -110
  71. mcp_agent/mcp/logger_textio.py +97 -0
  72. mcp_agent/mcp/mcp_agent_client_session.py +7 -7
  73. mcp_agent/mcp/mcp_agent_server.py +8 -8
  74. mcp_agent/mcp/mcp_aggregator.py +102 -143
  75. mcp_agent/mcp/mcp_connection_manager.py +20 -27
  76. mcp_agent/mcp/prompt_message_multipart.py +68 -16
  77. mcp_agent/mcp/prompt_render.py +77 -0
  78. mcp_agent/mcp/prompt_serialization.py +30 -48
  79. mcp_agent/mcp/prompts/prompt_constants.py +18 -0
  80. mcp_agent/mcp/prompts/prompt_helpers.py +327 -0
  81. mcp_agent/mcp/prompts/prompt_load.py +109 -0
  82. mcp_agent/mcp/prompts/prompt_server.py +155 -195
  83. mcp_agent/mcp/prompts/prompt_template.py +35 -66
  84. mcp_agent/mcp/resource_utils.py +7 -14
  85. mcp_agent/mcp/sampling.py +17 -17
  86. mcp_agent/mcp_server/agent_server.py +13 -17
  87. mcp_agent/mcp_server_registry.py +13 -22
  88. mcp_agent/resources/examples/{workflows → in_dev}/agent_build.py +3 -2
  89. mcp_agent/resources/examples/in_dev/slides.py +110 -0
  90. mcp_agent/resources/examples/internal/agent.py +6 -3
  91. mcp_agent/resources/examples/internal/fastagent.config.yaml +8 -2
  92. mcp_agent/resources/examples/internal/job.py +2 -1
  93. mcp_agent/resources/examples/internal/prompt_category.py +1 -1
  94. mcp_agent/resources/examples/internal/prompt_sizing.py +3 -5
  95. mcp_agent/resources/examples/internal/sizer.py +2 -1
  96. mcp_agent/resources/examples/internal/social.py +2 -1
  97. mcp_agent/resources/examples/prompting/agent.py +2 -1
  98. mcp_agent/resources/examples/prompting/image_server.py +4 -8
  99. mcp_agent/resources/examples/prompting/work_with_image.py +19 -0
  100. mcp_agent/ui/console_display.py +16 -20
  101. fast_agent_mcp-0.1.12.dist-info/RECORD +0 -161
  102. mcp_agent/core/agent_app.py +0 -646
  103. mcp_agent/core/agent_utils.py +0 -71
  104. mcp_agent/core/decorators.py +0 -455
  105. mcp_agent/core/factory.py +0 -463
  106. mcp_agent/core/proxies.py +0 -269
  107. mcp_agent/core/types.py +0 -24
  108. mcp_agent/eval/__init__.py +0 -0
  109. mcp_agent/mcp/stdio.py +0 -111
  110. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +0 -188
  111. mcp_agent/resources/examples/data-analysis/analysis.py +0 -65
  112. mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +0 -41
  113. mcp_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -1471
  114. mcp_agent/resources/examples/mcp_researcher/researcher-eval.py +0 -53
  115. mcp_agent/resources/examples/researcher/fastagent.config.yaml +0 -66
  116. mcp_agent/resources/examples/researcher/researcher-eval.py +0 -53
  117. mcp_agent/resources/examples/researcher/researcher-imp.py +0 -190
  118. mcp_agent/resources/examples/researcher/researcher.py +0 -38
  119. mcp_agent/resources/examples/workflows/chaining.py +0 -44
  120. mcp_agent/resources/examples/workflows/evaluator.py +0 -78
  121. mcp_agent/resources/examples/workflows/fastagent.config.yaml +0 -24
  122. mcp_agent/resources/examples/workflows/human_input.py +0 -25
  123. mcp_agent/resources/examples/workflows/orchestrator.py +0 -73
  124. mcp_agent/resources/examples/workflows/parallel.py +0 -78
  125. mcp_agent/resources/examples/workflows/router.py +0 -53
  126. mcp_agent/resources/examples/workflows/sse.py +0 -23
  127. mcp_agent/telemetry/__init__.py +0 -0
  128. mcp_agent/telemetry/usage_tracking.py +0 -18
  129. mcp_agent/workflows/__init__.py +0 -0
  130. mcp_agent/workflows/embedding/__init__.py +0 -0
  131. mcp_agent/workflows/embedding/embedding_base.py +0 -61
  132. mcp_agent/workflows/embedding/embedding_cohere.py +0 -49
  133. mcp_agent/workflows/embedding/embedding_openai.py +0 -46
  134. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  135. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +0 -481
  136. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  137. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +0 -120
  138. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +0 -134
  139. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +0 -45
  140. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +0 -45
  141. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +0 -161
  142. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +0 -60
  143. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +0 -60
  144. mcp_agent/workflows/llm/__init__.py +0 -0
  145. mcp_agent/workflows/llm/augmented_llm.py +0 -753
  146. mcp_agent/workflows/llm/augmented_llm_passthrough.py +0 -241
  147. mcp_agent/workflows/llm/augmented_llm_playback.py +0 -109
  148. mcp_agent/workflows/llm/providers/__init__.py +0 -8
  149. mcp_agent/workflows/llm/sampling_format_converter.py +0 -22
  150. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  151. mcp_agent/workflows/orchestrator/orchestrator.py +0 -578
  152. mcp_agent/workflows/parallel/__init__.py +0 -0
  153. mcp_agent/workflows/parallel/fan_in.py +0 -350
  154. mcp_agent/workflows/parallel/fan_out.py +0 -187
  155. mcp_agent/workflows/parallel/parallel_llm.py +0 -166
  156. mcp_agent/workflows/router/__init__.py +0 -0
  157. mcp_agent/workflows/router/router_base.py +0 -368
  158. mcp_agent/workflows/router/router_embedding.py +0 -240
  159. mcp_agent/workflows/router/router_embedding_cohere.py +0 -59
  160. mcp_agent/workflows/router/router_embedding_openai.py +0 -59
  161. mcp_agent/workflows/router/router_llm.py +0 -320
  162. mcp_agent/workflows/swarm/__init__.py +0 -0
  163. mcp_agent/workflows/swarm/swarm.py +0 -320
  164. mcp_agent/workflows/swarm/swarm_anthropic.py +0 -42
  165. mcp_agent/workflows/swarm/swarm_openai.py +0 -41
  166. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/WHEEL +0 -0
  167. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/entry_points.txt +0 -0
  168. {fast_agent_mcp-0.1.12.dist-info → fast_agent_mcp-0.2.0.dist-info}/licenses/LICENSE +0 -0
  169. /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)