fast-agent-mcp 0.4.7__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/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,353 @@
|
|
|
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, Tuple, Type
|
|
9
|
+
|
|
10
|
+
from mcp import Tool
|
|
11
|
+
from opentelemetry import trace
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from fast_agent.agents.agent_types import AgentConfig, AgentType
|
|
15
|
+
from fast_agent.agents.llm_agent import LlmAgent
|
|
16
|
+
from fast_agent.core.exceptions import AgentConfigError
|
|
17
|
+
from fast_agent.core.logging.logger import get_logger
|
|
18
|
+
from fast_agent.core.prompt import Prompt
|
|
19
|
+
from fast_agent.interfaces import FastAgentLLMProtocol, LLMFactoryProtocol, ModelT
|
|
20
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from a2a.types import AgentCard
|
|
24
|
+
|
|
25
|
+
from fast_agent.context import Context
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
# Simple system instruction for the router
|
|
30
|
+
ROUTING_SYSTEM_INSTRUCTION = """
|
|
31
|
+
You are a highly accurate request router that directs incoming requests to the most appropriate agent.
|
|
32
|
+
Analyze each request and determine which specialized agent would be best suited to handle it based on their capabilities.
|
|
33
|
+
|
|
34
|
+
Follow these guidelines:
|
|
35
|
+
- Carefully match the request's needs with each agent's capabilities and description
|
|
36
|
+
- Select the single most appropriate agent for the request
|
|
37
|
+
- Provide your confidence level (high, medium, low) and brief reasoning for your selection
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Default routing instruction with placeholders for context (AgentCard JSON)
|
|
41
|
+
ROUTING_AGENT_INSTRUCTION = """
|
|
42
|
+
Select from the following agents to handle the request:
|
|
43
|
+
<fastagent:agents>
|
|
44
|
+
[
|
|
45
|
+
{context}
|
|
46
|
+
]
|
|
47
|
+
</fastagent:agents>
|
|
48
|
+
|
|
49
|
+
You must respond with the 'name' of one of the agents listed above.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RoutingResponse(BaseModel):
|
|
55
|
+
"""Model for the structured routing response from the LLM."""
|
|
56
|
+
|
|
57
|
+
agent: str
|
|
58
|
+
confidence: str
|
|
59
|
+
reasoning: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RouterAgent(LlmAgent):
|
|
63
|
+
"""
|
|
64
|
+
A simplified router that uses an LLM to determine the best agent for a request,
|
|
65
|
+
then dispatches the request to that agent and returns the response.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def agent_type(self) -> AgentType:
|
|
70
|
+
"""Return the type of this agent."""
|
|
71
|
+
return AgentType.ROUTER
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
config: AgentConfig,
|
|
76
|
+
agents: List[LlmAgent],
|
|
77
|
+
routing_instruction: str | None = None,
|
|
78
|
+
context: "Context | None" = None,
|
|
79
|
+
default_request_params: RequestParams | None = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Initialize a RouterAgent.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: Agent configuration or name
|
|
87
|
+
agents: List of agents to route between
|
|
88
|
+
routing_instruction: Optional custom routing instruction
|
|
89
|
+
context: Optional application context
|
|
90
|
+
default_request_params: Optional default request parameters
|
|
91
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(config=config, context=context, **kwargs)
|
|
94
|
+
|
|
95
|
+
if not agents:
|
|
96
|
+
raise AgentConfigError("At least one agent must be provided")
|
|
97
|
+
|
|
98
|
+
self.agents = agents
|
|
99
|
+
self.routing_instruction = routing_instruction
|
|
100
|
+
self.agent_map = {agent.name: agent for agent in agents}
|
|
101
|
+
|
|
102
|
+
# Set up base router request parameters with just the base instruction for now
|
|
103
|
+
base_params = {"systemPrompt": ROUTING_SYSTEM_INSTRUCTION, "use_history": False}
|
|
104
|
+
|
|
105
|
+
if default_request_params:
|
|
106
|
+
merged_params = default_request_params.model_copy(update=base_params)
|
|
107
|
+
else:
|
|
108
|
+
merged_params = RequestParams(**base_params)
|
|
109
|
+
|
|
110
|
+
self._default_request_params = merged_params
|
|
111
|
+
|
|
112
|
+
async def initialize(self) -> None:
|
|
113
|
+
"""Initialize the router and all agents."""
|
|
114
|
+
if not self.initialized:
|
|
115
|
+
await super().initialize()
|
|
116
|
+
|
|
117
|
+
# Initialize all agents if not already initialized
|
|
118
|
+
for agent in self.agents:
|
|
119
|
+
if not agent.initialized:
|
|
120
|
+
await agent.initialize()
|
|
121
|
+
|
|
122
|
+
complete_routing_instruction = await self._generate_routing_instruction(
|
|
123
|
+
self.agents, self.routing_instruction
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Update the system prompt to include the routing instruction with agent cards
|
|
127
|
+
combined_system_prompt = (
|
|
128
|
+
ROUTING_SYSTEM_INSTRUCTION + "\n\n" + complete_routing_instruction
|
|
129
|
+
)
|
|
130
|
+
self._default_request_params.systemPrompt = combined_system_prompt
|
|
131
|
+
self.instruction = combined_system_prompt
|
|
132
|
+
|
|
133
|
+
async def shutdown(self) -> None:
|
|
134
|
+
"""Shutdown the router and all agents."""
|
|
135
|
+
await super().shutdown()
|
|
136
|
+
|
|
137
|
+
# Shutdown all agents
|
|
138
|
+
for agent in self.agents:
|
|
139
|
+
try:
|
|
140
|
+
await agent.shutdown()
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Error shutting down agent: {str(e)}")
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
async def _generate_routing_instruction(
|
|
146
|
+
agents: List[LlmAgent], routing_instruction: Optional[str] = None
|
|
147
|
+
) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Generate the complete routing instruction with agent cards.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
agents: List of agents to include in routing instruction
|
|
153
|
+
routing_instruction: Optional custom routing instruction template
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Complete routing instruction with agent cards formatted
|
|
157
|
+
"""
|
|
158
|
+
# Generate agent descriptions
|
|
159
|
+
agent_descriptions = []
|
|
160
|
+
for agent in agents:
|
|
161
|
+
agent_card: AgentCard = await agent.agent_card()
|
|
162
|
+
agent_descriptions.append(
|
|
163
|
+
agent_card.model_dump_json(
|
|
164
|
+
include={"name", "description", "skills"}, exclude_none=True
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
context = ",\n".join(agent_descriptions)
|
|
169
|
+
|
|
170
|
+
# Format the routing instruction
|
|
171
|
+
instruction_template = routing_instruction or ROUTING_AGENT_INSTRUCTION
|
|
172
|
+
return instruction_template.format(context=context)
|
|
173
|
+
|
|
174
|
+
async def attach_llm(
|
|
175
|
+
self,
|
|
176
|
+
llm_factory: LLMFactoryProtocol,
|
|
177
|
+
model: str | None = None,
|
|
178
|
+
request_params: RequestParams | None = None,
|
|
179
|
+
**additional_kwargs,
|
|
180
|
+
) -> FastAgentLLMProtocol:
|
|
181
|
+
return await super().attach_llm(
|
|
182
|
+
llm_factory, model, request_params, verb="Routing", **additional_kwargs
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def generate_impl(
|
|
186
|
+
self,
|
|
187
|
+
messages: List[PromptMessageExtended],
|
|
188
|
+
request_params: Optional[RequestParams] = None,
|
|
189
|
+
tools: List[Tool] | None = None,
|
|
190
|
+
) -> PromptMessageExtended:
|
|
191
|
+
"""
|
|
192
|
+
Route the request to the most appropriate agent and return its response.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
normalized_messages: Already normalized list of PromptMessageExtended
|
|
196
|
+
request_params: Optional request parameters
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
The response from the selected agent
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# implementation note. the duplication between generated and structured
|
|
203
|
+
# is probably the most readable. alt could be a _get_route_agent or
|
|
204
|
+
# some form of dynamic dispatch.. but only if this gets more complex
|
|
205
|
+
tracer = trace.get_tracer(__name__)
|
|
206
|
+
with tracer.start_as_current_span(f"Routing: '{self.name}' generate"):
|
|
207
|
+
route, warn = await self._route_request(messages[-1])
|
|
208
|
+
|
|
209
|
+
if not route:
|
|
210
|
+
return Prompt.assistant(warn or "No routing result or warning received")
|
|
211
|
+
|
|
212
|
+
# Get the selected agent
|
|
213
|
+
agent: LlmAgent = self.agent_map[route.agent]
|
|
214
|
+
|
|
215
|
+
# Dispatch the request to the selected agent
|
|
216
|
+
# discarded request_params: use llm defaults for subagents
|
|
217
|
+
telemetry_arguments = {
|
|
218
|
+
"agent": route.agent,
|
|
219
|
+
"confidence": route.confidence,
|
|
220
|
+
"reasoning": route.reasoning,
|
|
221
|
+
}
|
|
222
|
+
async with self.workflow_telemetry.start_step(
|
|
223
|
+
"router.delegate",
|
|
224
|
+
server_name=self.name,
|
|
225
|
+
arguments=telemetry_arguments,
|
|
226
|
+
) as step:
|
|
227
|
+
if route.reasoning:
|
|
228
|
+
await step.update(message=route.reasoning)
|
|
229
|
+
result = await agent.generate_impl(messages)
|
|
230
|
+
await step.finish(
|
|
231
|
+
True,
|
|
232
|
+
text=f"Delegated to {agent.name}",
|
|
233
|
+
)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
async def structured_impl(
|
|
237
|
+
self,
|
|
238
|
+
messages: List[PromptMessageExtended],
|
|
239
|
+
model: Type[ModelT],
|
|
240
|
+
request_params: Optional[RequestParams] = None,
|
|
241
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
242
|
+
"""
|
|
243
|
+
Route the request to the most appropriate agent and parse its response.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
messages: Messages to route
|
|
247
|
+
model: Pydantic model to parse the response into
|
|
248
|
+
request_params: Optional request parameters
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The parsed response from the selected agent, or None if parsing fails
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
tracer = trace.get_tracer(__name__)
|
|
255
|
+
with tracer.start_as_current_span(f"Routing: '{self.name}' structured"):
|
|
256
|
+
route, warn = await self._route_request(messages[-1])
|
|
257
|
+
|
|
258
|
+
if not route:
|
|
259
|
+
return None, Prompt.assistant(
|
|
260
|
+
warn or "No routing result or warning received (structured)"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Get the selected agent
|
|
264
|
+
agent: LlmAgent = self.agent_map[route.agent]
|
|
265
|
+
|
|
266
|
+
# Dispatch the request to the selected agent
|
|
267
|
+
telemetry_arguments = {
|
|
268
|
+
"agent": route.agent,
|
|
269
|
+
"confidence": route.confidence,
|
|
270
|
+
"reasoning": route.reasoning,
|
|
271
|
+
}
|
|
272
|
+
async with self.workflow_telemetry.start_step(
|
|
273
|
+
"router.delegate_structured",
|
|
274
|
+
server_name=self.name,
|
|
275
|
+
arguments=telemetry_arguments,
|
|
276
|
+
) as step:
|
|
277
|
+
if route.reasoning:
|
|
278
|
+
await step.update(message=route.reasoning)
|
|
279
|
+
structured_response = await agent.structured_impl(messages, model, request_params)
|
|
280
|
+
await step.finish(
|
|
281
|
+
True,
|
|
282
|
+
text=f"{agent.name} produced structured output",
|
|
283
|
+
)
|
|
284
|
+
return structured_response
|
|
285
|
+
|
|
286
|
+
async def _route_request(
|
|
287
|
+
self, message: PromptMessageExtended
|
|
288
|
+
) -> Tuple[RoutingResponse | None, str | None]:
|
|
289
|
+
"""
|
|
290
|
+
Determine which agent to route the request to.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
request: The request to route
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
RouterResult containing the selected agent, or None if no suitable agent was found
|
|
297
|
+
"""
|
|
298
|
+
if not self.agents:
|
|
299
|
+
logger.error("No agents available for routing")
|
|
300
|
+
raise AgentConfigError("No agents available for routing - fatal error")
|
|
301
|
+
|
|
302
|
+
# go straight to agent if only one available
|
|
303
|
+
if len(self.agents) == 1:
|
|
304
|
+
return RoutingResponse(
|
|
305
|
+
agent=self.agents[0].name, confidence="high", reasoning="Only one agent available"
|
|
306
|
+
), None
|
|
307
|
+
|
|
308
|
+
assert self._llm
|
|
309
|
+
# Display the user's routing request
|
|
310
|
+
self.display.show_user_message(message.first_text(), name=self.name)
|
|
311
|
+
|
|
312
|
+
# No need to add routing instruction here - it's already in the system prompt
|
|
313
|
+
response, _ = await self._llm.structured(
|
|
314
|
+
[message],
|
|
315
|
+
RoutingResponse,
|
|
316
|
+
self._default_request_params,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
warn: str | None = None
|
|
320
|
+
if not response:
|
|
321
|
+
warn = "No routing response received from LLM"
|
|
322
|
+
elif response.agent not in self.agent_map:
|
|
323
|
+
warn = f"A response was received, but the agent {response.agent} was not known to the Router"
|
|
324
|
+
|
|
325
|
+
if warn:
|
|
326
|
+
logger.warning(warn)
|
|
327
|
+
return None, warn
|
|
328
|
+
else:
|
|
329
|
+
assert response
|
|
330
|
+
logger.info(
|
|
331
|
+
f"Routing structured request to agent: {response.agent or 'error'} (confidence: {response.confidence or ''})"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
routing_message = f"Routing to: {response.agent}"
|
|
335
|
+
if response.reasoning:
|
|
336
|
+
routing_message += f" ({response.reasoning})"
|
|
337
|
+
|
|
338
|
+
# Convert highlight_items to highlight_index
|
|
339
|
+
agent_keys = list(self.agent_map.keys())
|
|
340
|
+
highlight_index = None
|
|
341
|
+
try:
|
|
342
|
+
highlight_index = agent_keys.index(response.agent)
|
|
343
|
+
except ValueError:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
await self.display.show_assistant_message(
|
|
347
|
+
routing_message,
|
|
348
|
+
bottom_items=agent_keys,
|
|
349
|
+
highlight_index=highlight_index,
|
|
350
|
+
name=self.name,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
return response, None
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from fast_agent.cli.constants import GO_SPECIFIC_OPTIONS, KNOWN_SUBCOMMANDS
|
|
6
|
+
from fast_agent.cli.main import app
|
|
7
|
+
|
|
8
|
+
# if the arguments would work with "go" we'll just route to it
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
"""Main entry point that handles auto-routing to 'go' command."""
|
|
13
|
+
try:
|
|
14
|
+
loop = asyncio.get_event_loop()
|
|
15
|
+
|
|
16
|
+
def _log_asyncio_exception(loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("fast_agent.asyncio")
|
|
20
|
+
|
|
21
|
+
message = context.get("message", "(no message)")
|
|
22
|
+
task = context.get("task")
|
|
23
|
+
future = context.get("future")
|
|
24
|
+
handle = context.get("handle")
|
|
25
|
+
source_traceback = context.get("source_traceback")
|
|
26
|
+
exception = context.get("exception")
|
|
27
|
+
|
|
28
|
+
details = {
|
|
29
|
+
"message": message,
|
|
30
|
+
"task": repr(task) if task else None,
|
|
31
|
+
"future": repr(future) if future else None,
|
|
32
|
+
"handle": repr(handle) if handle else None,
|
|
33
|
+
"source_traceback": [str(frame) for frame in source_traceback] if source_traceback else None,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.error("Unhandled asyncio error: %s", message)
|
|
37
|
+
logger.error("Asyncio context: %s", json.dumps(details, indent=2))
|
|
38
|
+
|
|
39
|
+
if exception:
|
|
40
|
+
logger.exception("Asyncio exception", exc_info=exception)
|
|
41
|
+
|
|
42
|
+
loop.set_exception_handler(_log_asyncio_exception)
|
|
43
|
+
except RuntimeError:
|
|
44
|
+
# No running loop yet (rare for sync entry), safe to ignore
|
|
45
|
+
pass
|
|
46
|
+
# Check if we should auto-route to 'go'
|
|
47
|
+
if len(sys.argv) > 1:
|
|
48
|
+
# Check if first arg is not already a subcommand
|
|
49
|
+
first_arg = sys.argv[1]
|
|
50
|
+
|
|
51
|
+
# Only auto-route if any known go-specific options are present
|
|
52
|
+
has_go_options = any(
|
|
53
|
+
(arg in GO_SPECIFIC_OPTIONS) or any(arg.startswith(opt + "=") for opt in GO_SPECIFIC_OPTIONS)
|
|
54
|
+
for arg in sys.argv[1:]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if first_arg not in KNOWN_SUBCOMMANDS and has_go_options:
|
|
58
|
+
# Find where to insert 'go' - before the first go-specific option
|
|
59
|
+
insert_pos = 1
|
|
60
|
+
for i, arg in enumerate(sys.argv[1:], 1):
|
|
61
|
+
if (arg in GO_SPECIFIC_OPTIONS) or any(
|
|
62
|
+
arg.startswith(opt + "=") for opt in GO_SPECIFIC_OPTIONS
|
|
63
|
+
):
|
|
64
|
+
insert_pos = i
|
|
65
|
+
break
|
|
66
|
+
# Auto-route to go command
|
|
67
|
+
sys.argv.insert(insert_pos, "go")
|
|
68
|
+
|
|
69
|
+
app()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Dedicated entry point for running FastAgent in ACP mode."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from fast_agent.cli.commands import serve
|
|
9
|
+
from fast_agent.cli.commands.go import (
|
|
10
|
+
collect_stdio_commands,
|
|
11
|
+
resolve_instruction_option,
|
|
12
|
+
run_async_agent,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
help="Run FastAgent as an ACP stdio server without specifying --transport=acp explicitly.",
|
|
17
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ROOT_SUBCOMMANDS = {
|
|
21
|
+
"go",
|
|
22
|
+
"serve",
|
|
23
|
+
"setup",
|
|
24
|
+
"check",
|
|
25
|
+
"auth",
|
|
26
|
+
"bootstrap",
|
|
27
|
+
"quickstart",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.callback(invoke_without_command=True, no_args_is_help=False)
|
|
32
|
+
def run_acp(
|
|
33
|
+
ctx: typer.Context,
|
|
34
|
+
name: str = typer.Option("fast-agent-acp", "--name", help="Name for the ACP server"),
|
|
35
|
+
instruction: str | None = typer.Option(
|
|
36
|
+
None, "--instruction", "-i", help="Path to file or URL containing instruction for the agent"
|
|
37
|
+
),
|
|
38
|
+
config_path: str | None = typer.Option(None, "--config-path", "-c", help="Path to config file"),
|
|
39
|
+
servers: str | None = typer.Option(
|
|
40
|
+
None, "--servers", help="Comma-separated list of server names to enable from config"
|
|
41
|
+
),
|
|
42
|
+
urls: str | None = typer.Option(
|
|
43
|
+
None, "--url", help="Comma-separated list of HTTP/SSE URLs to connect to"
|
|
44
|
+
),
|
|
45
|
+
auth: str | None = typer.Option(
|
|
46
|
+
None, "--auth", help="Bearer token for authorization with URL-based servers"
|
|
47
|
+
),
|
|
48
|
+
model: str | None = typer.Option(
|
|
49
|
+
None, "--model", "--models", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
|
|
50
|
+
),
|
|
51
|
+
skills_dir: Path | None = typer.Option(
|
|
52
|
+
None,
|
|
53
|
+
"--skills-dir",
|
|
54
|
+
"--skills",
|
|
55
|
+
help="Override the default skills directory",
|
|
56
|
+
),
|
|
57
|
+
npx: str | None = typer.Option(
|
|
58
|
+
None, "--npx", help="NPX package and args to run as MCP server (quoted)"
|
|
59
|
+
),
|
|
60
|
+
uvx: str | None = typer.Option(
|
|
61
|
+
None, "--uvx", help="UVX package and args to run as MCP server (quoted)"
|
|
62
|
+
),
|
|
63
|
+
stdio: str | None = typer.Option(
|
|
64
|
+
None, "--stdio", help="Command to run as STDIO MCP server (quoted)"
|
|
65
|
+
),
|
|
66
|
+
description: str | None = typer.Option(
|
|
67
|
+
None,
|
|
68
|
+
"--description",
|
|
69
|
+
"-d",
|
|
70
|
+
help="Description used for the exposed send tool (use {agent} to reference the agent name)",
|
|
71
|
+
),
|
|
72
|
+
host: str = typer.Option(
|
|
73
|
+
"0.0.0.0",
|
|
74
|
+
"--host",
|
|
75
|
+
help="Host address to bind when using HTTP or SSE transport",
|
|
76
|
+
),
|
|
77
|
+
port: int = typer.Option(
|
|
78
|
+
8000,
|
|
79
|
+
"--port",
|
|
80
|
+
help="Port to use when running as a server with HTTP or SSE transport",
|
|
81
|
+
),
|
|
82
|
+
shell: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--shell",
|
|
85
|
+
"-x",
|
|
86
|
+
help="Enable a local shell runtime and expose the execute tool (bash or pwsh).",
|
|
87
|
+
),
|
|
88
|
+
instance_scope: serve.InstanceScope = typer.Option(
|
|
89
|
+
serve.InstanceScope.SHARED,
|
|
90
|
+
"--instance-scope",
|
|
91
|
+
help="Control how ACP clients receive isolated agent instances (shared, connection, request)",
|
|
92
|
+
),
|
|
93
|
+
no_permissions: bool = typer.Option(
|
|
94
|
+
False,
|
|
95
|
+
"--no-permissions",
|
|
96
|
+
help="Disable tool permission requests (allow all tool executions without asking)",
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Run FastAgent with ACP transport defaults.
|
|
101
|
+
|
|
102
|
+
This mirrors `fast-agent serve --transport acp` but provides a shorter command and
|
|
103
|
+
a distinct default name so ACP-specific tooling can integrate more easily.
|
|
104
|
+
"""
|
|
105
|
+
stdio_commands = collect_stdio_commands(npx, uvx, stdio)
|
|
106
|
+
shell_enabled = shell
|
|
107
|
+
|
|
108
|
+
resolved_instruction, agent_name = resolve_instruction_option(instruction)
|
|
109
|
+
|
|
110
|
+
run_async_agent(
|
|
111
|
+
name=name,
|
|
112
|
+
instruction=resolved_instruction,
|
|
113
|
+
config_path=config_path,
|
|
114
|
+
servers=servers,
|
|
115
|
+
urls=urls,
|
|
116
|
+
auth=auth,
|
|
117
|
+
model=model,
|
|
118
|
+
message=None,
|
|
119
|
+
prompt_file=None,
|
|
120
|
+
stdio_commands=stdio_commands,
|
|
121
|
+
agent_name=agent_name,
|
|
122
|
+
skills_directory=skills_dir,
|
|
123
|
+
shell_enabled=shell_enabled,
|
|
124
|
+
mode="serve",
|
|
125
|
+
transport=serve.ServeTransport.ACP.value,
|
|
126
|
+
host=host,
|
|
127
|
+
port=port,
|
|
128
|
+
tool_description=description,
|
|
129
|
+
instance_scope=instance_scope.value,
|
|
130
|
+
permissions_enabled=not no_permissions,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
"""Console script entrypoint for `fast-agent-acp`."""
|
|
136
|
+
# Override Click's UsageError exit code from 2 to 1 for consistency
|
|
137
|
+
import click
|
|
138
|
+
|
|
139
|
+
click.exceptions.UsageError.exit_code = 1
|
|
140
|
+
|
|
141
|
+
args = sys.argv[1:]
|
|
142
|
+
if args and args[0] in ROOT_SUBCOMMANDS:
|
|
143
|
+
from fast_agent.cli.__main__ import main as root_cli_main
|
|
144
|
+
|
|
145
|
+
root_cli_main()
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
# Run the Typer app without triggering automatic sys.exit so we can
|
|
149
|
+
# guarantee error output goes to stderr with a non-zero exit code.
|
|
150
|
+
app(standalone_mode=False)
|
|
151
|
+
except click.ClickException as exc:
|
|
152
|
+
# Preserve Typer's rich formatting when available, otherwise fall back to plain text.
|
|
153
|
+
try:
|
|
154
|
+
import typer.rich_utils as rich_utils
|
|
155
|
+
|
|
156
|
+
rich_utils.rich_format_error(exc)
|
|
157
|
+
except Exception:
|
|
158
|
+
exc.show(file=sys.stderr)
|
|
159
|
+
sys.exit(getattr(exc, "exit_code", 1))
|